Your agent needs three operations of one API. The API has a typed SDK and an MCP server, and the
first draft goes through the server. Every request to the model then carries the server’s whole
tool list, and when the API renames a field the agent reads undefined without an error. Wiring
the SDK directly fixes both, but every other client then has to be wired again.
What you get
You will end up with the same three operations wired as in-process typed tools and as MCP tools, measured for startup, tokens per request, and behavior under an API rename. This is for you if you build one agent over an API that already has an SDK.
Short answer
Call the SDK directly when one agent you compile owns the integration. The tools then cost nothing to start, carry only the operations the task needs, and a renamed field fails the type check. Go through an MCP server when several clients must share the same tools. Then filter the server’s tool list on the client, because every listed definition travels in every request.
You will need
Node 22 or later, Go 1.25 or later to build the catalog’s generated server, and an API with the
three operations your task needs. Verified 2026-10-21 against Node 22.22.0, Go 1.25.0,
@modelcontextprotocol/server 2.3.1, @modelcontextprotocol/client 2.3.1, Zod 4.6.7, gpt-tokenizer
4.1.0, TypeScript 5.9.3, and the Open-Meteo go-mcp module v0.0.7. The API is a stand-in on
127.0.0.1 with nine operations and fixed data, and the model is a scripted policy, so no request
leaves the machine. The SDK, servers, and transports are real.
Approaches compared
| Approach | When it fits | What it costs you | When to pick something else |
|---|---|---|---|
| In-process typed SDK tools | One agent, one codebase, and operations you choose by hand | A tool layer every other client has to repeat, and input schemas you write beside the SDK’s own types | Several clients need the same tools, or the tools must outlive this agent |
| Stdio MCP server built with the official MCP SDK | Tools shared with editors and desktop clients on the same machine | A Node process to spawn per client, JSON text in place of typed values, and every listed tool in every request until the client filters | The agent is the only consumer, or it runs where it cannot spawn a process |
| Streamable HTTP MCP server built with the official MCP SDK | Tools shared across machines and teams from one deployment | A service to run, secure, and keep up, plus a network hop on every tool call | Everything runs in one process on one machine |
| Voxgig SDK catalog Go MCP server | A public API in the catalog, and you want its operations as tools without writing a server | A Go build, and a generated tool surface shaped by the catalog’s entity model rather than by your task | The API is not in the catalog, or the task needs tools named for its operations |
Voxgig maintains the SDK catalog. It is one of four options here, not the recommendation.
The in-process layer is the cheapest to start and the hardest to share: it lives in one process, and every other client rewrites it. An MCP server over the same SDK is shared by any client that speaks the protocol. It pays in a process or a service to run, text in place of types, and a tool list the client has to trim. The catalog’s generated server saves writing one, and its tools are the catalog’s two generic operations rather than your three.
Wire the three operations twice
The direct layer wraps three methods of the typed client as tools for the loop. Each tool trims the record to what the model needs, and the field names come from the client’s types.
{
name: 'getCharger',
description: 'Fetch one charger by id.',
inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] },
execute: async ({ id }) => {
const c = await sdk.getCharger(id as string)
// Trim to the fields the model needs. The names come from the client's types.
return { id: c.id, site: c.site, kw: c.kw, status: c.status }
},
},
The MCP server calls the same client. Like a generated server, it registers every operation the
client has, nine here, with the
registerTool call of
@modelcontextprotocol/server, and returns each result as the JSON text the protocol carries.
server.registerTool('listChargers', { description: 'List chargers, optionally filtered by site.', inputSchema: z.object({ site: z.string().optional() }) },
({ site }) => guard(() => sdk.listChargers({ site }))())
server.registerTool('getCharger', { description: 'Fetch one charger by id.', inputSchema: z.object({ id: z.string() }) },
({ id }) => guard(() => sdk.getCharger(id))())
guard turns the client’s ApiError into a result with isError: true, the
tools specification’s
way for a tool to report a failure the model should read. The agent’s side connects, lists the
tools, and keeps the ones the task names:
const chosen = allow ? tools.filter((t) => allow.includes(t.name)) : tools
Run the task through both routes, each against a fresh copy of the API. The loop is shared, and a scripted policy stands in for the model.
node --experimental-strip-types demo.ts
in-process typed SDK
tools the model sees: listChargers, getCharger, startSession
call listChargers({"site":"Cork"})
result [{"id":"chg_cork_01","site":"Cork","kw":50,"status":"available"},{"id":"chg_cork_02","site":"Cork","kw":22,"status":"offline"}]
call getCharger({"id":"chg_cork_01"})
result {"id":"chg_cork_01","site":"Cork","kw":50,"status":"available"}
call startSession({"chargerId":"chg_cork_01"})
result {"id":"ses_1001","chargerId":"chg_cork_01","status":"charging","startedAt":"2026-06-01T09:00:00Z"}
final Started session ses_1001 on chg_cork_01 at Cork (50 kW).
requests the API saw: GET /chargers?site=Cork, GET /chargers/chg_cork_01, POST /sessions
stdio MCP server, three tools allowed
tools the model sees: listChargers, getCharger, startSession
call listChargers({"site":"Cork"})
result [{"id":"chg_cork_01","site":"Cork","kw":50,"status":"available"},{"id":"chg_cork_02","site":"Cork","kw":22,"status":"offline"}]
call getCharger({"id":"chg_cork_01"})
result {"id":"chg_cork_01","site":"Cork","kw":50,"status":"available"}
call startSession({"chargerId":"chg_cork_01"})
result {"id":"ses_1001","chargerId":"chg_cork_01","status":"charging","startedAt":"2026-06-01T09:00:00Z"}
final Started session ses_1001 on chg_cork_01 at Cork (50 kW).
requests the API saw: GET /chargers?site=Cork, GET /chargers/chg_cork_01, POST /sessions
The two transcripts match line for line, and so do the requests the API received. The routes differ in what the model is not shown: the process boundary, the serialization, and the number of definitions in each request.1
Build the generated server and measure
The catalog’s server for the Open-Meteo API is a Go program on the
official MCP Go SDK, in the
go-mcp directory of its repository. The
sample’s go.mod names it with a tool directive,
so one command builds it.
go build -o bin/open-meteo-mcp github.com/voxgig-sdk/open-meteo-sdk/go-mcp && ls bin
open-meteo-mcp
The measurement runs the same task through every route and times the step each needs before the
loop can start. Tokens are counted with the o200k encoding of
gpt-tokenizer, a stand-in for the model’s own. The
catalog server gets OPEN_METEO_BASE pointing at a second stand-in, so its one tool call stays
on the machine.
node --experimental-strip-types measure.ts
in-process typed SDK
startup duration_ms: 0.3
tools available: 3, sent to the model: 3
tool definitions: 110 tokens per request
task: 4 requests, 440 tokens of definitions, 105 tokens of results
stdio MCP server, every tool
startup duration_ms: 208.8
tools available: 9, sent to the model: 9
tool definitions: 485 tokens per request
task: 4 requests, 1940 tokens of definitions, 105 tokens of results
stdio MCP server, three tools allowed
startup duration_ms: 209.1
tools available: 9, sent to the model: 3
tool definitions: 159 tokens per request
task: 4 requests, 636 tokens of definitions, 105 tokens of results
Streamable HTTP MCP server, three tools allowed
startup duration_ms: 71.5
tools available: 9, sent to the model: 3
tool definitions: 159 tokens per request
task: 4 requests, 636 tokens of definitions, 105 tokens of results
catalog Go MCP server (Open-Meteo)
startup duration_ms: 11.4
listed by the server: open-meteo_list, open-meteo_load
tool definitions: 231 tokens per request
open-meteo_load asked the API for: GET /v1/forecast?latitude=51.9&longitude=-8.47
result: { "current": { "temperature_2m": 12.4, "time": "2026-06-01T09:00" }, "latitude": 51.9, "longitude": -8.47 }
The token lines repeat on every request for the life of the agent, so read them first. The
three hand-written definitions cost 110 tokens. The same three through the server cost 159,
because the server stamps a $schema URL on every input schema it lists.2 Taking all nine
costs 485 per request, and over a four-request task that is 1,940 tokens of definitions against
105 tokens of results. The durations are this machine’s: a few hundred milliseconds to spawn a
Node server, about ten for the Go binary, and a fraction of a millisecond in process. The HTTP
figure leaves out its server’s own start.
The catalog server lists two tools, open-meteo_list and open-meteo_load, each taking an
entity name and a query map, and the load call reached the stand-in as GET /v1/forecast.
That is the catalog’s shape for every API: generic operations over an entity model, not one tool
per endpoint.
Rename a field and watch each route
The API renames kw to powerKw, and the MCP server is regenerated against the new client.
#sdk in package.json resolves to the regenerated client under the v2
condition for both processes, while the
agent’s own code still reads kw.
node --conditions=v2 --experimental-strip-types change.ts
getCharger returned: {"id":"chg_cork_01","site":"Cork","powerKw":50,"status":"available"}
requests: 4, failed results: 0
final answer: Started session ses_1001 on chg_cork_01 at Cork (undefined kW).
The run completed in four requests without a failed result. The answer carries undefined
because the scripted policy reads kw by name from text that, over MCP, no compiler checks. A
model would read powerKw in the same text, so the failure lands in code that reads a result by
field name. The direct layer is compiled against the client, and tsc with the same condition
finds the read before anything runs:
npx tsc -p tsconfig.v2.json
direct.ts(22,48): error TS2339: Property 'kw' does not exist on type 'Charger'.
tsconfig.v2.json sets
customConditions to v2, so the
compiler resolves #sdk the way Node does. Node’s
type stripping runs the same file unchecked, so the
check belongs in the build, not the run.
Check it worked
Six of nine tests, with the Node test runner, pin what the
three runs showed. The three routes make identical API calls and give the same answer, and nine
definitions cost more than twice what three cost. A failed call reaches the loop as a failed
result under both routes, the rename passes through MCP unnoticed, tsc rejects the direct
layer, and the catalog server answers a load. The other three check that the client refuses an id
of .., the HTTP server a foreign Origin or Host, and that a server dying before it listens
fails the wiring.
node --experimental-strip-types --test wiring.test.ts
1..9
# tests 9
# suites 0
# pass 9
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 6062.496114
The last test skips itself when bin/open-meteo-mcp is missing.
When it goes wrong
Answers and screens built from tool results show undefined or a blank after a deployment. A
server you do not own was regenerated, and code in the agent reads a field name from before the
change. Pin the server version you tested against, and validate each tool result against your own
schema before anything reads it; the type check covers the direct layer only.
The client lists no tools from the stdio server, or waits forever. Something other than a message
was written to stdout, usually a console.log left in for debugging. The
stdio binding
makes stdout the wire, so log to stderr.
go build prints go: downloading go1.25.0 before it compiles anything. The catalog module’s
go.mod declares go 1.25.0, and a Go 1.24 installation fetches that toolchain under the
default toolchain rules rather than refusing.3 Install Go 1.25
where the build runs, or let the download happen once.
When not to do this
Do not put an MCP server between one agent and one API when you compile both. The server costs a process or a service, turns typed values into text, and hands the model a tool list it did not ask for. It buys a sharing you do not need yet; add the server when a second client appears.
Do not take every tool a server lists. The second step shows nine definitions costing more than four times the task’s results on every request. The fix is one filter on the client, in code where the list can be reviewed.
Do not point the agent at the catalog’s Go MCP server expecting tools named for your operations.
It exposes two generic tools, list and load, over the catalog’s entity model: good for exploring
an API, wrong for a task that wants startSession by name. The catalog’s SDKs are also
unofficial and regenerated on a schedule you do not set, so pin the version you tested against.
Do not read the startup figures as a verdict. They vary by machine, and a running server costs nothing to start. The token figures repeat on every request, which is why they decide more.
Related how-tos
Last verified
Verified 2026-10-21 against Node 22.22.0, Go 1.25.0, @modelcontextprotocol/server 2.3.1, @modelcontextprotocol/client 2.3.1, Zod 4.6.7, gpt-tokenizer 4.1.0, TypeScript 5.9.3, and github.com/voxgig-sdk/open-meteo-sdk/go-mcp v0.0.7. Every output block is what the command preceding it printed. The API and the Open-Meteo endpoint are stand-ins on 127.0.0.1 and the model is a scripted policy, so the transcripts reproduce exactly and the durations do not. Token counts use the o200k encoding, not any model’s own tokenizer.
Footnotes
-
The Claude Agent SDK draws the same line inside one process. Its TypeScript reference defines custom tools with
tool()and groups them withcreateSdkMcpServer(), an MCP server instance that runs in the caller’s process, passed to the agent undermcpServerswithtype: "sdk". The in-process option there is an MCP server with the transport removed, so in that SDK the choice reduces to a process boundary. ↩︎ Back to text -
The stamp is
"$schema": "https://json-schema.org/draft/2020-12/schema", 49 tokens under o200k across the three tools, sixteen or seventeen apiece by what precedes it. JSON Schema’s core specification wants the keyword in a document’s root schema object and permits it in the root of an embedded schema resource. A tool’s input schema is a root to the server that emits it and an embedded object to the request that carries it, and a tokenizer counts it either way. ↩︎ Back to text -
The toolchain rules say that a
goline newer than the installed toolchain, in the main module or a dependency, makes thegocommand download that toolchain and run it. The catalog’sgo-mcpmodule declaresgo 1.25.0, sogo getfrom a 1.24 module rewrote the sample’s owngoline to1.25.0and then fetched go1.25.0 before building a 15 MB binary. ↩︎ Back to text