MCP in PHP - Connect an Agent to a Remote Tool Server in 4 Lines
If you build AI tools in more than one language, you know the tax: every integration you write for Python, you re-write for PHP. MCP (Model Context Protocol) kills that tax. It's a standard protocol for exposing tools, so your PHP agent talks to the same tool servers a Python, Node, or Go agent would. You don't re-implement integrations per language - you point at a server and let the agent discover the tools.
This is the shortest path to a genuinely capable agent in PHP.
The four lines
1. Connect to the MCP server
McpClient just points at a URL - no handshake code, no auth setup for a public server like this one.
use NanoAgent\Agent;
use NanoAgent\Mcp\McpClient;
$mcpClient = new McpClient('https://mcp.deepwiki.com/mcp');
2. Build the agent as usual
Nothing MCP-specific here yet - it's a normal Agent with a system prompt describing what it's for.
$agent = new Agent(
llm: $llmConfig,
systemPrompt: "You are a research assistant with access to MCP tools for "
. "exploring GitHub repositories. The user will name a repository "
. "as 'owner/repo'. Use the available tools to answer their question."
);
3. Register the server, then chat
registerMcpServer() asks the remote server what tools it offers and wires them into the agent - that's the "discovery" step. From here, calling chat() works exactly like it would with local tools.
$discovered = $agent->registerMcpServer($mcpClient);
$agent->enableActivityLogging();
$answer = $agent->chat("Repository: facebook/react\nQuestion: What does this repo do?");
Connect → register → chat. The agent discovers the server's tools at runtime (that's what registerMcpServer() returns) and calls them exactly like local FunctionTools. No tool schema by hand, no new DSL, no sidecar process.
Registering less, not more
Discovery is convenient, but not free. Every tool you register adds its full JSON Schema to every request the agent makes from then on - a 50-tool server means 50 tool definitions in each payload, which is wasted tokens (and wasted latency) if the agent only ever calls two of them.
So: what's on the server?
Before you can allowlist anything you need the names, and there are two ways to get them.
Ask the server without registering. McpClient::listTools() is public and registers nothing - it just reports what the server exposes, so it's safe to call at any time:
$mcpClient = new McpClient('https://mcp.deepwiki.com/mcp');
foreach ($mcpClient->listTools() as $tool) {
echo $tool['name'], "\n"; // -> the name you put in the allowlist
echo ($tool['description'] ?? ''), "\n"; // -> so you know what it's for
}
Each entry is ['name' => ..., 'description' => ..., 'inputSchema' => [...]] - the same schema the agent would send to the model.
Or register everything and read the names back. Omitting the allowlist registers the whole server, and the return value tells you what you got. Handy for a one-off look at a server you don't know yet:
$names = $agent->registerMcpServer($mcpClient); // no allowlist -> registers all
print_r($names); // then re-register narrowed
Keep the same $mcpClient object for both calls. The MCP handshake is memoized per client instance, so re-using it skips a second initialize round trip - the tools/list call itself is repeated either way.
There is deliberately no Agent::getTools(): once a tool is registered its wrapper is an implementation detail, and the return value of registerMcpServer() is the listing you actually want - the names, not the objects.
Then allowlist
registerMcpServer() takes an optional allowlist. Name the tools you want; everything else is left alone:
// Register everything (the default):
$all = $agent->registerMcpServer($mcpClient);
// Or only what you actually want:
$some = $agent->registerMcpServer($mcpClient, allowlist: ['ask_question']);
A name the server doesn't expose is ignored, not an error - which is exactly what you want when the allowlist comes out of a config file and one tool was retired upstream.
Calling the method again on the same client reconciles rather than duplicates. Tools you keep stay registered, tools you drop are removed from the agent, and any tool you registered locally yourself is never touched. That makes it safe to call once per request with an allowlist that comes from configuration.
What the agent is talking to here
The example uses DeepWiki's public MCP server - free, no auth - which exposes tools for asking questions about the source and docs of any public GitHub repository. So the agent above can genuinely read and summarize an unfamiliar codebase. Point the same four lines at any other MCP server (search, databases, file systems, your own services) and the agent picks up that server's tools instead.
Local tool vs. MCP tool - the comparison
| | FunctionTool (local) | MCP tool (remote) |
|---|---|---|
| Where it runs | in your PHP process | on a remote server |
| Who defines it | you, in PHP | the MCP server |
| Reuse across languages | no | yes - one server, any client |
| Auth / infra | none | server handles it |
| Discovery | you list them | automatic via registerMcpServer() |
You use local tools for logic that lives in your app (your DB, your business rules). You use MCP for capabilities that should be shared, versioned, or maintained by someone else.
Why this matters for PHP specifically
PHP shops don't leave PHP to add an AI feature. MCP means they don't have to build the feature either - point at a server that already has the tool. The agent is a thin PHP layer over a standard protocol, which is exactly the shape you want for a language that has no AI framework ecosystem.
Getting started
new McpClient($url).$agent->registerMcpServer($client) - add allowlist: [...] if the server exposes more tools than you want to pay for.$client->listTools() prints them without registering anything.That's the whole integration.
Ready to ship an agent?
composer require hammrouni/nanoagent
MIT, zero dependencies. Explore the full series - each article teaches one pattern.
Get each new article by email - one per publish day + the 12-part cheatsheet when it drops.