Skip to content

phpboyscout · go toolkit

Attach MCP to the tools you already have

One module puts a Go application on the Model Context Protocol: a Cobra CLI with one mounted command, an HTTP service with one handler, a gRPC service with one binding per method. Publish progressively, three discovery tools whatever the size of the catalogue, or wide, one native tool per operation, over the same registry, policy and results.

go get gitlab.com/phpboyscout/go/mcp
mcp — a lantern lighting a tree of nodes one step at a time

The thirty-second version

A tool with eighty commands. Here is what a client is carrying before the user has typed a word, in each of the two shapes this module publishes:

{"tools": [
  {"name": "mytool_build",   "inputSchema": {"type": "object", "properties": {"target": ..., "verbose": ..., "jobs": ...}}},
  {"name": "mytool_deploy",  "inputSchema": {"type": "object", "properties": {"env": ..., "region": ..., "dry_run": ...}}},
  {"name": "mytool_rollback", "inputSchema": {"type": "object", "properties": {"env": ..., "to": ...}}},
  ... 77 more, schemas and all, in the context of every conversation
]}
{"tools": [
  {"name": "search_tools",     "description": "Find authorized operations by query or group. Empty query browses; inspect a name for its schema."},
  {"name": "get_tool_details", "description": "Inspect one authorized operation, including its application input and output schemas. ..."},
  {"name": "call_tool",        "description": "Invoke an authorized operation by name and arguments. Prior search is optional. ..."}
]}

Eighty schemas, most of them never used, is what the wide catalogue costs, and it grows with the tool. This module publishes three tools by default, whatever the size of the catalogue. The model searches, inspects the one operation it needs, and calls it; a schema enters the conversation when it is about to be used, and only that one. Nothing in the protocol knows this is happening, so it works with every client that speaks MCP.

The wide shape is one option away when a client wants every tool up front: server.WithMode(server.Direct) publishes each operation as a native tool, with its own schema and hints, over the same registry, the same policy and the same results. Which to choose is a how-to; progressive discovery has the reasoning and the trade it carries.

What you get

Attaches to what you have A Cobra CLI mounts one command and every exposed command is an operation. An HTTP service mounts one handler under its own middleware and lifecycle. A gRPC service binds each unary method with one call, and every MCP call runs through the service's own interceptors and authorisation predicate, bounded by the MCP call's deadline and on its trace.
Two shapes, one registry Progressive by default: search_tools, get_tool_details, call_tool, and the context cost stays flat however large the catalogue. Wide on request: WithMode(Direct) publishes one native tool per operation. Policy, execution and results are identical in both.
Authorisation asked every time Your policy is asked on every search page, inspection and call, with the validated arguments. A name a caller may not run gets the same answer as a name that does not exist, so nothing probes the catalogue.
Bounded execution A bound Cobra command runs as a child process with one execution slot, a deadline, an output budget, and cleanup of every process it started. Arguments never reach a shell.
Rich results Six content variants (text, image, audio, resource link, embedded text and blob), structured JSON kept separate from content, exact numbers preserved on the wire, and presentation hints for an Apps shell.
Confirmations and forms A handler suspends with a form or URL input request and resumes with the answer, ten rounds by default; replay is safe and a client without the capability gets an actionable fallback.
Resources Explicit bindings with their own policy, asked per item before reading and again before delivering; bounded partial bundles that say what is missing without leaking what is hidden.
Mounts where you already are One http.Handler under your transport, transit chain and controls controller: your body limit applies, a foreign Origin is refused, a client's disconnect cancels the operation, and shutdown drains in-flight calls.
Editor helpers mcp claude, mcp cursor and mcp vscode write and remove the editor's entry, leave unrelated configuration alone and repeat safely; mcp tools exports the catalogue.
A framework-free root The operation model imports no protocol SDK, Cobra, HTTP or lifecycle framework, and a test keeps it so. A CLI and a service share the model and nothing else.

→ The full case: why it exists and what it assumes

At a glance

A Cobra CLI mounts one command and is an MCP server:

import (
    mcpcli "gitlab.com/phpboyscout/go/mcp/cli"
    mcpcobra "gitlab.com/phpboyscout/go/mcp/cobra"
)

root.AddCommand(mcpcli.Command(mcpcli.WithBinding(mcpcobra.WithExposure(isExposed))))

my-tool mcp start serves every exposed command over stdio, my-tool mcp stream serves it over HTTP under a controls lifecycle, my-tool mcp tools exports the catalogue, and my-tool mcp vscode enable registers the server with the editor.

A service registers its operations explicitly and mounts one handler beside its own routes:

registry, err := mcp.New(operations, mcp.WithPolicy(policy)) // policy reads your middleware's identity
host, err := server.New(registry, server.WithIdentity("studio", version))

mux.Handle("/mcp", host.Handler())            // behind the chain you already run
controller.Register("mcp",                    // after the transport, so calls cancel before the listener drains
    controls.WithStopErr(host.Shutdown),
    controls.WithStatus(host.Status),
)

Should you use this?

Yes, if any of these describe you:

  • you build tools on go-tool-base: its mcp feature is this module, and every tool built on it serves MCP this way;
  • you have a Cobra CLI with more commands than a model should carry in every conversation, and you want them run as they would be from a terminal, isolated and bounded;
  • you run an HTTP or gRPC service and want to expose chosen operations under the authentication and lifecycle it already has, not a second server;
  • your operations return media, need a confirmation before a side effect, or hand back a bundle of files rather than a string.

Probably not, if a handful of tools with no authorisation, no subprocess and no lifecycle to fit into is all you need. The official Go SDK on its own is the smaller dependency there, and this module builds on it rather than replacing it.

Where next

The documentation follows the Diátaxis quadrants: learn, then do, then understand, then look up.

Status

v0.1.0 is the first release: the operation model, the SDK adapter over stdio and Streamable HTTP in both publication modes, the Cobra binding and the mcp command tree. go-tool-base consumes it on main, so every tool built on its next release serves MCP through this module. The mounted-service shape is proven by the module's own tests; native macOS and Windows subprocess acceptance and browser Apps acceptance are wired but not yet run, and the development reference says what that means. The authoritative design is spec 0001; the specification register on the project wiki links the reports behind it.

The Go API reference, every type, method and signature, lives on pkg.go.dev, generated from the source it documents so it cannot drift from it.

Further reading

The blog carries a curated route through this subject: Building a command-line tool in Go collects everything written about it, ordered so you can start at the beginning rather than newest-first.

Ask phpbotscout

phpbotscout

He answers questions about the projects over on the Discord, citing the docs where they already cover it, and offering to raise an issue where they don't. Bring a bug, an idea, or a questionable engineering decision.

Join the Discord