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.
| Tool | Description |
|---|---|
app_get_info |
PHP and Nette versions, installed Nette packages, directories and database driver; the agent is told to call it first |
| Tool | Description |
|---|---|
di_get_services |
List services with types, tags, aliases and autowiring, optionally filtered by name or type |
di_get_service |
Details of one service, including whether it has been created |
di_find_by_type |
Services implementing a class or interface |
di_find_by_tag |
Services carrying a tag, with the tag values |
di_get_parameter_names |
Parameter names, nested ones in dotted notation (database.default.dsn) |
di_get_parameter |
Value 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.
| Tool | Description |
|---|---|
router_get_routes |
List all registered routes with masks, defaults and module prefixes |
router_match_url |
Match a URL to presenter/action and parameters (e.g. /article/123) |
router_generate_url |
Generate 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/.
| Tool | Description |
|---|---|
db_get_tables |
List tables and views |
db_get_columns |
Columns of a table: types, nullability, defaults, primary and foreign keys |
db_get_relationships |
Foreign key relationships between all tables (belongsTo, hasMany) |
db_get_indexes |
Indexes of a table |
db_query |
Run a single SQL statement with values bound to ? placeholders; read-only by default (see below) |
db_explain_query |
Run 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.
| Tool | Description |
|---|---|
tracy_get_log |
Newest entries of a log by level (exception by default, error, warning, …), each with the name of its report |
tracy_get_report |
An 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).
composer require --dev nette/mcp-inspector
vendor/bin/mcp-inspector initinit creates three files and never overwrites an existing one:
| File | Purpose |
|---|---|
mcp-bootstrap.php |
returns your application's Nette\Bootstrap\Configurator (see below) |
config/mcp-inspector.neon |
the inspector's configuration: what the agent may do |
.mcp.json |
registers 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.
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
BootstrapwithbootWebApplication(): 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.phpis thenreturn (new App\Bootstrap)->bootConfigurator();. -
Custom bootstrap (instance constructor, multi-tenant…):
initwrites a template with a TODO. Keepnew Configuratorinside your Bootstrap class so Nette's%appDir%autodetection (based on the file that instantiates it) stays correct. Environment variables from.mcp.jsonare 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.
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: 100The 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).
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:
- install it as a
--devdependency:composer install --no-devon the server never installs it; - the defaults are safe: nothing modifies data, secrets are masked, no tool executes PHP code or writes files;
- an explicit kill switch:
inspector: enabled: falseinconfig/mcp-inspector.neonor theMCP_INSPECTOR_DISABLED=1environment variable makes the server refuse to start.
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.
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.jsonbyinit(defaults tophp)
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