How-to › Use AI to do the integration

How to choose between calling an SDK directly and using MCP#

Wire three API operations into one agent as in-process typed tools and as MCP tools, then measure startup, tokens per request, and what a rename breaks under each.

Audience
Agent builder
Level
intermediate
Topic
Build an agent that calls APIs
Languages
TypeScript and Go
Verified

Read first: Build a tool-calling loop with the Claude Agent SDK

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

ApproachWhen it fitsWhat it costs youWhen to pick something else
In-process typed SDK toolsOne agent, one codebase, and operations you choose by handA tool layer every other client has to repeat, and input schemas you write beside the SDK’s own typesSeveral clients need the same tools, or the tools must outlive this agent
Stdio MCP server built with the official MCP SDKTools shared with editors and desktop clients on the same machineA Node process to spawn per client, JSON text in place of typed values, and every listed tool in every request until the client filtersThe agent is the only consumer, or it runs where it cannot spawn a process
Streamable HTTP MCP server built with the official MCP SDKTools shared across machines and teams from one deploymentA service to run, secure, and keep up, plus a network hop on every tool callEverything runs in one process on one machine
Voxgig SDK catalog Go MCP serverA public API in the catalog, and you want its operations as tools without writing a serverA Go build, and a generated tool surface shaped by the catalog’s entity model rather than by your taskThe 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.

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

  1. The Claude Agent SDK draws the same line inside one process. Its TypeScript reference defines custom tools with tool() and groups them with createSdkMcpServer(), an MCP server instance that runs in the caller’s process, passed to the agent under mcpServers with type: "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

  2. 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

  3. The toolchain rules say that a go line newer than the installed toolchain, in the main module or a dependency, makes the go command download that toolchain and run it. The catalog’s go-mcp module declares go 1.25.0, so go get from a 1.24 module rewrote the sample’s own go line to 1.25.0 and then fetched go1.25.0 before building a 15 MB binary. ↩︎ Back to text

Read this page as markdown · All how-to guides

Generate the client instead of writing it#

Retries, timeouts, pagination and auth are the same problems in every client. Voxgig generates them from your OpenAPI description, in 23 languages, from one model.

Listen to Fireside, the Voxgig podcast

Since 2018, Richard Rodger has talked with developer advocates, event organizers, and the people who build developer-facing companies about the specifics of the work.