If you give a Goose an MCP server

If you give a Goose an MCP server 图片 1
If you give a Goose an MCP server 图片 2
If you give a Goose an MCP server 图片 3

A hands-on build: register a local model, federate your MCP servers into one endpoint, scope that endpoint down to the six tools the job needs, and watch every model and tool call land in one log, tagged with the key that made it. Built on agentgateway.

If you give a Goose an MCP server, it gets every tool the server ships.

You handed it a filesystem server because it needed to read one file. It got the read tool, and the write tool, and the edit tool, and 11 more, because tools/list came back full and nobody reads a tool manifest the way they read a diff. The filesystem server exposes 14 tools. The Goose now has all 14, because the server decides what the Goose can do, and that server was written to do everything.

This is a build for taking that decision back, by hand, once, so the tool layer stops being a black box you accept on faith. You put a gateway in the one place every call has to cross. You register your model there, so model calls ride the same layer as tool calls. You federate your MCP servers into a single endpoint and grant one agent exactly six tools on it. Then you watch the whole session, model calls and tool calls alike, land in one log, each row tagged with the key that made it. By the end the Goose holds six tools it can't step outside of, and a dashboard shows you every one it reached for.

The gateway is agentgateway, a single Rust binary and an AAIF project. It bills itself as a connectivity solution for agents: one proxy in front of model traffic, MCP traffic, and ordinary HTTP, applying the same auth, policy, and logging to all three. That "all three" is what makes this build work. The model and the tools go through one door, so one log holds the whole session. I'm running it in standalone mode — a single binary and a YAML file, with an admin UI at:15000/ui that hot-reloads on save — in front of Goose talking to a local model. The six-tool set I grant the Goose has a name (that I made up): tiny-tools.

One important thing to note: the agent that builds this needs a shell and room to install things, so the builder is you, or an agent that still has the run of the house. The Goose we keep narrowing to six tools is the subject on the workbench. You hold the wrench.

Everything here is one config file. The slices nest like this, top-level keys first:

config: database: url: "sqlite:///path/to/requests.db?mode=rwc" # the request-log DB (Step 5) standardAttributes: user: 'apiKey.name' # stamps every logged row with the calling key's name

frontendPolicies: accessLog: database: add: { prompt: 'llm.prompt', completion: 'llm.completion' } # capture prompts + completions (Step 5)

llm: # Step 1 -- the model listener port: 15003 providers: models: policies: apiKey: {... } # the goose key, so model calls are keyed and attributed

mcp: # Step 2 -- the federated tool listener port: 15001 targets: policies: apiKey: {... } # Step 3 -- the per-agent keys (goose, cc) mcpAuthorization: {... } # Step 3 -- match the key name to scope its tools

Before you start. Install agentgateway and get it running. Save your config as config.yaml and launch with agentgateway -f config.yaml; the admin UI at:15000/ui hot-reloads on every save, and agentgateway -f config.yaml --validate-only checks a config before you run it. Have Ollama serving a tool-capable model ( ollama pull qwen3:8b ), install Goose, and keep npx (for the filesystem server) and uvx (for the fetch server) within reach.

Step 1: Put your model behind the gateway

Start with the model. In my case, I'll use Ollama, just because I'm used to it, but you can use anything that serves a model. Routing it through the gateway is the piece most local setups skip, and it decides whether you can watch the whole session later. Route the model through the gateway and its calls land in the same request log as the tool calls, tagged with the same key. Leave it pointed at Ollama directly and you get a curated tool layer with a blind spot where the model should be.

An agentgateway llm block is a provider plus a list of models. The provider names your backend once; each model references it. The listener also carries the goose key, so a model call arrives identified:

llm: port: 15003 providers: • name: local provider: ollama params: baseUrl:localhost:11434/v1 models: • name: qwen3:8b provider: reference: local policies: apiKey: mode: strict keys: • key: sk-... # the same goose key you'll reuse in Step 3 metadata: { name: goose }

The gateway now exposes an OpenAI-compatible API on:15003, and every model call through it is one row in the request log, stamped goose by standardAttributes.user. Point Goose at the gateway with four environment variables:

export GOOSE_PROVIDER=openai export OPENAI_HOST=http://localhost:15003 # bare host:port; Goose appends the OpenAI path export OPENAI_API_KEY=sk-... # the goose key -- our LLM listener is keyed export GOOSE_MODEL=qwen3:8b

Agentgateway's own Goose guide uses a placeholder key here, because its example listener takes any key. Ours doesn't. The goose key is what stamps every model row with the same identity the tool calls carry, so it has to be the real one.

Agent: in agentgateway's llm config, register an Ollama provider pointed at my local Ollama ( baseUrl localhost:11434/v1 ), add my model referencing it, and attach an apiKey policy carrying the goose key so model calls are authenticated and attributed. Set standardAttributes.user: 'apiKey.name' in the top-level config block. Then point Goose at the gateway with GOOSE_PROVIDER=openai, OPENAI_HOST=http://localhost:15003, OPENAI_API_KEY=, GOOSE_MODEL=qwen3:8b. Read agentgateway's Ollama provider docs and its Goose integration guide.

Step 2: Federate your MCP servers into one endpoint

Now the model has somewhere to reach. Give it tools. The interesting part is how you give them. You federate several MCP servers behind the gateway and hand the agent a single endpoint, rather than wiring one connection per server into the client. The gateway multiplexes them. The agent connects to one place, and the servers behind it are the gateway's concern.

You define each server as a target under the mcp block. Two small servers here, a filesystem server scoped to one workspace directory and a fetch server:

mcp: port: 15001 targets: • name: filesystem stdio: cmd: npx args: ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/workspace"] • name: fetch stdio: cmd: uvx args: ["mcp-server-fetch"]

Between them, that's 15 tools behind one endpoint. The gateway namespaces each tool by its target, so the filesystem server's read_text_file shows up as filesystem_read_text_file. Open the Servers view and both targets register and report ready. The Goose connects to this one endpoint, never to either server directly.

Agent: federate two stdio MCP targets under one mcp endpoint: @modelcontextprotocol/server-filesystem scoped to a single workspace directory (target name filesystem ) and mcp-server-fetch (target name fetch ). Confirm both report ready and that the combined tools/list shows all 15 tools, target-prefixed. Read agentgateway's MCP multiplexing docs; a target name can't contain underscores.

Step 3: Scope the endpoint per agent

Give a Goose 15 tools and it will find a use for one you never meant it to have. So before the Goose ever connects, you decide what's on the endpoint for it, and you decide it per key. One virtual endpoint, and each API key gets its own view of it.

Let's scope the MCP tools into custom virtual MCP servers for two different agents: one on Goose, and one in Claude Code. We're picking the Goose's six by hand for now; Step 7 shows how to derive that set from real traffic instead.

policies: # under the mcp block from Step 2 apiKey: mode: strict keys: • key: sk-... # the Goose's key (the same one from Step 1) metadata: { name: goose } • key: sk-... # Claude Code's key metadata: { name: cc }

Then we match on the key's name to scope which tools each one sees…

添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论