Nette MCP Inspector

August 27, 2026 · View on GitHub

MCP (Model Context Protocol) server for Nette application introspection. It lets AI assistants such as Claude Code look into your application: its DI container, configuration, routing, database schema and logs.

⚠️ Development tool only. The inspector exposes the DI graph, configuration and data of whatever application it is pointed at. Point it at development environments and development data only.

Tools

Application

ToolDescription
app_get_infoPHP and Nette versions, installed Nette packages, directories and database driver; the agent is told to call it first

DI container

ToolDescription
di_get_servicesList services with types, tags, aliases and autowiring, optionally filtered by name or type
di_get_serviceDetails of one service, including whether it has been created
di_find_by_typeServices implementing a class or interface
di_find_by_tagServices carrying a tag, with the tag values
di_get_parameter_namesParameter names, nested ones in dotted notation (database.default.dsn)
di_get_parameterValue of one parameter; secrets (password, token, dsn, …) are masked

Requires nette/di ≥ 3.2.7 (Container::getServiceDescriptors()). Only runtime data is available: compile-time facts such as factory expressions and setup calls are not.

Router

ToolDescription
router_get_routesList all registered routes with masks, defaults and module prefixes
router_match_urlMatch a URL to presenter/action and parameters (e.g. /article/123)
router_generate_urlGenerate the URL for presenter/action, like {link} in templates (e.g. Article:show, {"id": 5})

The inspector runs without an HTTP request, so the application cannot detect its own address the way it does on the web. Tell it in the application's configuration (nette/http 3.4):

http:
	baseUrl: https://example.com/

Without it router_generate_url reports an error saying so, and relative URLs passed to router_match_url are matched against http://localhost/.

Database

ToolDescription
db_get_tablesList tables and views
db_get_columnsColumns of a table: types, nullability, defaults, primary and foreign keys
db_get_relationshipsForeign key relationships between all tables (belongsTo, hasMany)
db_get_indexesIndexes of a table
db_queryRun a single SQL statement with values bound to ? placeholders; read-only by default (see below)
db_explain_queryRun EXPLAIN on a SELECT query

The only security question is whether the agent may modify data, and by default it may not (database: readOnly: true): db_query accepts only SELECT, SHOW, EXPLAIN, DESCRIBE, WITH, VALUES and TABLE statements (single statement, no INTO OUTFILE), and additionally runs them inside a read-only transaction (START TRANSACTION READ ONLY on MySQL and PostgreSQL, PRAGMA query_only on SQLite), so the database itself rejects anything the validator would miss; other drivers rely on the validator alone. With readOnly: false any statement runs. Values of columns whose names suggest secrets (password, token, …) are masked.

Tracy

ToolDescription
tracy_get_logNewest entries of a log by level (exception by default, error, warning, …), each with the name of its report
tracy_get_reportAn exception report as Tracy 2.12+ writes it for agents: markdown with the code around the exception, the stack trace with arguments and the environment

The log directory is the one the application's Tracy logger writes to; nothing to configure. Requires tracy/tracy ≥ 2.12 for the markdown reports (older HTML-only reports cannot be read).

Setup

composer require --dev nette/mcp-inspector
vendor/bin/mcp-inspector init

init creates three files and never overwrites an existing one:

FilePurpose
mcp-bootstrap.phpreturns your application's Nette\Bootstrap\Configurator (see below)
config/mcp-inspector.neonthe inspector's configuration: what the agent may do
.mcp.jsonregisters the nette-inspector server for Claude Code (an existing file is extended); .cursor/mcp.json and .vscode/mcp.json get the same entry when those directories exist

Restart the Claude Code session afterwards. Use --php="ddev exec php" when PHP does not run on the host, --project=PATH when the project is not the current directory.

mcp-bootstrap.php

The file must return the Configurator with all configs added, before createContainer() is called; the inspector builds the container itself. init recognizes the common shapes of App\Bootstrap:

  • Static App\Bootstrap::boot(): Configurator (the classic Web Project): return App\Bootstrap::boot();

  • Object Bootstrap with bootWebApplication(): Container (Web Project since 2024): add a method that stops before creating the container, and use it from both places:

    public function bootWebApplication(): Nette\DI\Container
    {
    	return $this->bootConfigurator()->createContainer();
    }
    
    public function bootConfigurator(): Configurator
    {
    	$this->initializeEnvironment();
    	$this->setupContainer();
    	return $this->configurator;
    }
    

    mcp-bootstrap.php is then return (new App\Bootstrap)->bootConfigurator();.

  • Custom bootstrap (instance constructor, multi-tenant…): init writes a template with a TODO. Keep new Configurator inside your Bootstrap class so Nette's %appDir% autodetection (based on the file that instantiates it) stays correct. Environment variables from .mcp.json are a handy way to parameterize it:

    $blog = getenv('BLOG') === 'phpfashion' ? App\Blog::PhpFashion : App\Blog::LaTrine;
    return (new App\Bootstrap($blog))->bootConsoleConfigurator();
    

The inspector switches the Configurator to debug mode itself (the CLI never autodetects it), so it compiles its own container, separate from the web's cache.

config/mcp-inspector.neon

The inspector is itself a small Nette application: this file is the DI configuration of its own container, with the usual parameters:, services: and one section per toolkit. Every section is optional; a missing file means the defaults. This is the file init generates:

# Configuration of nette/mcp-inspector: a Nette DI config for the inspector's own container.
# The inspector reads it itself, do not add it to the application's configs.
# Every section is optional; missing keys use the defaults shown here.

inspector:
	# false keeps the inspector from starting at all
	enabled: true
	# tool names or patterns hidden from the agent, e.g. [db_*, tracy_get_log]
	disableTools: []

database:
	# true: only SELECT-like statements, run in a read-only transaction
	# false: any statement, the agent can modify data
	readOnly: true
	# maximum number of rows returned by db_query
	rowLimit: 100

The inspector reads this file itself; do not add it to the application's configs. Changes take effect after the MCP server restarts (Claude Code does that with the session).

Protection against running in production

There is no reliable runtime signal in a CLI process that would tell the inspector it is running on a production server, so the protection is layered instead:

  1. install it as a --dev dependency: composer install --no-dev on the server never installs it;
  2. the defaults are safe: nothing modifies data, secrets are masked, no tool executes PHP code or writes files;
  3. an explicit kill switch: inspector: enabled: false in config/mcp-inspector.neon or the MCP_INSPECTOR_DISABLED=1 environment variable makes the server refuse to start.

Live config reload

mcp-bootstrap.php runs once. On every tool call the inspector asks the Configurator for the container class: Nette checks the compiled container's dependencies (config files, touched classes), recompiles it when they changed and returns a freshly named class, which the inspector instantiates. When nothing changed, the same container instance is reused, so the per-call overhead is a metadata check. Config edits in common.neon, services.neon, etc. are therefore picked up live, without restarting Claude Code or the MCP server. This requires nette/di ≥ 3.2.7.

If a rebuild fails (e.g. a typo in your config), the inspector keeps serving the last-known-good container and adds a _warning field to tool results so you immediately see the failure message.

CLI

mcp-inspector [options]                       starts the MCP server (stdio)
mcp-inspector init [options]                  generates the project files
mcp-inspector call <tool> [<json>] [options]  calls one tool and prints the result

call runs a tool exactly as the MCP client would, without a client, so you can try a tool or check your bootstrap and configuration from the terminal:

vendor/bin/mcp-inspector call router_match_url '{"url": "/article/123"}'
  • --project=PATH — project root (defaults to the current working directory)
  • --bootstrap=PATH — bootstrap script (defaults to <project>/mcp-bootstrap.php)
  • --config=PATH — inspector configuration (defaults to <project>/config/mcp-inspector.neon)
  • --php=COMMAND — PHP command written to .mcp.json by init (defaults to php)

Custom toolkits

A toolkit is a class implementing Nette\McpInspector\Toolkit; its public methods marked with #[McpTool] become tools. Register it as a service in config/mcp-inspector.neon; it can depend on Nette\McpInspector\AppContainer, whose get() returns the current application container (so config reloads are honoured), or on any other inspector service. Mark a tool whose result carries data from the application (database rows, user input) with #[Nette\McpInspector\UntrustedData]; the inspector then tells the model not to follow instructions found in it. The tool's description comes from the docblock, its title and annotations from #[McpTool]; a toolkit whose annotations depend on its configuration implements Nette\McpInspector\ToolAnnotator:

namespace App\Mcp;

use Mcp\Capability\Attribute\McpTool;
use Mcp\Schema\ToolAnnotations;
use Nette\McpInspector\AppContainer;
use Nette\McpInspector\Toolkit;

class BlogToolkit implements Toolkit
{
	public function __construct(
		private AppContainer $app,
	) {}

	public function isAvailable(): bool
	{
		return true;
	}

	/**
	 * Get blog post by ID.
	 * @param int $id Post ID
	 */
	#[McpTool(name: 'blog_get_post', title: 'Blog post', annotations: new ToolAnnotations(readOnlyHint: true))]
	public function getPost(int $id): array
	{
		$post = $this->app->get()->getByType(BlogFacade::class)->getPost($id);
		return $post ? ['id' => $post->id, 'title' => $post->title] : ['error' => 'not found'];
	}
}
# config/mcp-inspector.neon
services:
	- App\Mcp\BlogToolkit