Tool Orchestration for AI Agents: MCP, Function Calling, and Beyond
Deep dive on agent tools: function calling across providers, MCP servers and clients, custom server authoring, and A2A patterns. Code included.
- PUBLISHED
- April 19, 2026
- READ TIME
- 11 MIN
- AUTHOR
- ONE FREQUENCY
An agent without tools is a chatbot. The choice of tool protocol determines how much engineering you spend on integration, how portable your agents are across providers, and how easily you reuse work across teams.
This article covers the three main approaches you will encounter: native function calling (OpenAI, Anthropic, Google), Model Context Protocol (MCP), and emerging agent-to-agent (A2A) protocols. You will see a working MCP server skeleton in TypeScript and a client integration with the Claude Agent SDK.
Native function calling: the per-provider view
Every frontier model has its own version of function calling. The shape is similar, the quirks are real.
OpenAI function calling
OpenAI uses a `tools` array on the request with JSON Schema for parameters. The model returns one or more `tool_calls` in the response. You execute them, pass results back in a follow-up call with `role: 'tool'` messages.
```json { "type": "function", "function": { "name": "search_orders", "description": "Search a customer's recent orders", "parameters": { "type": "object", "properties": { "customer_id": { "type": "string" }, "limit": { "type": "integer", "default": 10 } }, "required": ["customer_id"] } } } ```
Quirks: strict mode (`strict: true`) is supported but requires `additionalProperties: false` and all fields in `required`. Parallel tool calls are on by default, which can surprise you if your tools are not idempotent.
Anthropic tool use
Anthropic uses a `tools` array with the same JSON Schema shape. The model returns `tool_use` blocks inside the assistant message. You reply with `tool_result` content blocks in the next user turn.
```json { "name": "search_orders", "description": "Search a customer's recent orders", "input_schema": { "type": "object", "properties": { "customer_id": { "type": "string" }, "limit": { "type": "integer", "default": 10 } }, "required": ["customer_id"] } } ```
Quirks: Claude requires that every `tool_use` block be paired with a corresponding `tool_result` block in the next user message, in the same order. The Claude Agent SDK handles the loop for you, which is one reason teams adopt it.
Gemini function calling
Gemini uses `functionDeclarations` on the model with similar JSON Schema. Responses come back as `functionCall` parts. Reply with `functionResponse` parts.
```json { "name": "search_orders", "description": "Search a customer's recent orders", "parameters": { "type": "object", "properties": { "customer_id": { "type": "string" }, "limit": { "type": "integer" } }, "required": ["customer_id"] } } ```
Quirks: Gemini supports automatic function calling in some SDKs (the SDK runs your tool for you given a callable), which is convenient but adds a layer of indirection that can complicate observability.
What MCP is and why it exists
Model Context Protocol is an open spec for connecting LLM applications to tools, resources, and prompts in a standardized way. Anthropic published it in late 2024 and the ecosystem grew through 2025 and 2026.
The core idea: instead of writing custom integration code in every agent for every tool, you write an MCP server once. Any MCP-compatible client (Claude Desktop, Claude Code, Cursor, Zed, custom agents, even some IDEs) can use it.
MCP has three primitives:
- Tools are functions the model can call to take actions
- Resources are read-only data sources the model or user can pull into context
- Prompts are reusable prompt templates the user can invoke
MCP transports:
- stdio for local servers spawned as subprocesses
- HTTP / SSE for remote servers with auth, multi-user, and network reachability
Server discovery happens via client configuration. A client config typically lists which MCP servers to start, with auth credentials and arguments.
The MCP server ecosystem
By mid-2026 there are hundreds of public MCP servers and many more private ones. The high-value ones for most teams:
| Server | What it gives the agent | | --- | --- | | Filesystem | Read, write, and search local files | | GitHub | Issues, PRs, code search, repo metadata | | Slack | Read channels, send messages, search history | | Postgres | Read and (optionally) write database access | | Stripe | Customer, payment, subscription queries | | Linear | Issues, projects, cycles | | Atlassian | Jira issues, Confluence pages | | Sentry | Errors, releases, alerts | | Cloudflare | Workers, KV, R2, account management | | Brave Search | Web search |
You can self-host most of these or use the official providers' hosted versions. Hosted versions trade off some control for less ops burden.
Authoring a custom MCP server in TypeScript
The official `@modelcontextprotocol/sdk` makes server authoring straightforward. Here is a minimal but real server skeleton that exposes one tool and one resource.
```typescript import { Server } from '@modelcontextprotocol/sdk/server/index.js' import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js'
const server = new Server( { name: 'orders-mcp', version: '0.1.0' }, { capabilities: { tools: {}, resources: {} } }, )
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: [ { name: 'search_orders', description: "Search a customer's recent orders by customer ID.", inputSchema: { type: 'object', properties: { customer_id: { type: 'string', description: 'Customer ID' }, limit: { type: 'integer', default: 10, minimum: 1, maximum: 100 }, }, required: ['customer_id'], }, }, ], }))
server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name !== 'search_orders') { throw new Error(`Unknown tool: ${request.params.name}`) } const { customer_id, limit = 10 } = request.params.arguments as { customer_id: string limit?: number } // Replace with real DB call. Keep errors structured. const orders = await fetchOrders(customer_id, limit) return { content: [{ type: 'text', text: JSON.stringify(orders) }], } })
server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: 'orders://schema', name: 'Order schema', mimeType: 'application/json', }, ], }))
server.setRequestHandler(ReadResourceRequestSchema, async (request) => { if (request.params.uri !== 'orders://schema') { throw new Error(`Unknown resource: ${request.params.uri}`) } return { contents: [ { uri: 'orders://schema', mimeType: 'application/json', text: JSON.stringify(ORDER_SCHEMA), }, ], } })
async function main() { const transport = new StdioServerTransport() await server.connect(transport) }
main().catch((err) => { console.error(err) process.exit(1) }) ```
Two things to keep in mind. First, your tool implementation must be idempotent if it has side effects. Pass through a client-generated request ID. Second, return structured errors as text content with an error discriminator, not as exceptions, so the agent can recover.
MCP client integration with Claude Agent SDK
On the client side, the Claude Agent SDK accepts MCP servers as a configuration option. The SDK handles the protocol, surfaces tools to the model, executes them when the model requests, and feeds results back.
```typescript import { ClaudeAgentClient } from '@anthropic-ai/claude-agent-sdk'
const client = new ClaudeAgentClient({ model: 'claude-sonnet-4-5', mcpServers: { orders: { command: 'node', args: ['./mcp-servers/orders/dist/index.js'], env: { DATABASE_URL: process.env.DATABASE_URL }, }, github: { command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'], env: { GITHUB_TOKEN: process.env.GITHUB_TOKEN }, }, }, })
const result = await client.run({ prompt: 'Find the last 5 orders for customer cust_123 and open a GitHub issue summarizing any failed payments.', })
console.log(result.finalMessage) ```
That snippet wires two MCP servers (one custom, one official) into a single agent run. The model sees both sets of tools and chooses when to use which.
Comparing tool patterns
| Dimension | Bespoke per-agent tools | MCP-standardized | A2A protocol | | --- | --- | --- | --- | | Setup cost per integration | High | Low after first server | Medium | | Portability across agents | Low | High | High | | Portability across providers | Low (per-provider schemas) | High (MCP is provider-agnostic) | High | | Discovery and reuse | Manual | Server registry | Agent registry | | Auth and access control | Custom per tool | Per-server, transport-level | Per-agent, often OAuth | | Best for | Single agent, narrow scope | Platform with many agents and tools | Multi-agent systems with distinct ownership |
The A2A protocol (Agent-to-Agent) is an emerging spec for letting agents call other agents as if they were tools, with their own capabilities, auth, and lifecycle. It is the natural evolution once you have many MCP-equipped agents and want to compose them.
For most teams today, the right answer is: native function calling for the prototype, MCP once you have more than two or three agents sharing tools, A2A once you have agent teams that need to delegate work between organizations or business units.
Checklist for adopting MCP
- [ ] Identify the top 3-5 tool integrations your agents need
- [ ] Use official MCP servers where they exist (filesystem, GitHub, Postgres, etc.)
- [ ] Author custom MCP servers for proprietary systems
- [ ] Decide on transport: stdio for local trusted contexts, HTTP for shared/remote
- [ ] Implement structured error responses in every tool
- [ ] Add idempotency keys to side-effecting tools
- [ ] Wire MCP servers into your observability stack (log every call)
- [ ] Define auth boundaries: which agents can use which servers
- [ ] Set per-tool rate limits at the server, not at the agent
- [ ] Version your MCP server tool schemas; document breaking changes
How tool orchestration plays into the rest of the platform
The hard part of running agents in production is not the model, it is the connective tissue: tools, observability, governance. Solid agent observability metrics need to capture every MCP call with arguments, latency, and outcome. The same metrics feed your cost analysis and your incident response.
MCP transport choices in production
The stdio transport is the easiest to start with. You spawn the server as a subprocess and pipe JSON-RPC over stdin/stdout. It works perfectly for single-user, local-trust contexts: developer machines, CI runners, single-tenant agent runtimes.
The HTTP / SSE (and now streamable HTTP) transport is what you need for multi-user, shared, or remote deployments. The server runs as a normal web service, often behind an API gateway, with OAuth or API key authentication. Multiple clients connect concurrently. You can deploy multiple instances behind a load balancer.
Decision rule: if your agent runs as a service that serves many users, your MCP servers should be HTTP. If your agent runs on a developer's laptop or in a single-tenant runtime, stdio is fine.
Versioning your MCP servers
Tool schemas are an API contract. Treat them like one.
- Add a version field to the server's name (e.g., `orders-mcp-v1`) so clients can pin
- Add new tools rather than changing existing tool signatures
- Mark deprecated tools clearly in their descriptions so the model avoids them
- Maintain at least one prior version during transition windows
- Document changes in a changelog clients can subscribe to
Agents are sensitive to tool description changes. A small wording change can shift tool selection in subtle ways. Run your eval suite on any non-trivial schema change before promoting.
Common MCP authoring mistakes
A short list of patterns to avoid:
- Vague tool descriptions. "Searches the database" is not enough. Say which database, which kind of records, what filters apply, and when the agent should choose this tool over alternatives.
- Overlapping tools. Two tools that do almost the same thing confuse the model. Consolidate.
- Free-text outputs for structured data. Return JSON. Let the agent parse it. Models are better at structured tool outputs than at re-parsing prose.
- Skipping idempotency. Side-effecting tools without idempotency keys lead to duplicate writes when the agent retries.
- Embedding secrets in tool inputs. Authenticate at the server level, not via tool arguments. The model should not have to handle credentials.
- Massive tool result payloads. Trim before returning. Pagination tokens beat dumping 10,000 rows back into context.
When not to use MCP
MCP is excellent for sharing tools across many agents and clients. It is overkill when:
- You have one agent and one tool integration that nobody else will use
- The tool is so latency-sensitive that the MCP overhead matters
- The tool's full surface depends on per-request user identity in a way that does not map cleanly to MCP's auth model
- You need fine-grained, per-call permission gates that are easier expressed in your agent runtime
For those cases, a direct in-process tool with the provider's native function calling is the simpler choice.
Next steps
If your team is wiring tools into agents and finds itself rewriting the same integration logic in three places, MCP is the right next layer. We help teams design tool architectures that scale across agents, providers, and business units. Reach out if you want a review of your tool layer before you commit to a direction.
Ready to ship the next outcome?
One Frequency Consulting brings 25+ years of technology leadership and military discipline to every engagement. First call is operator-grade scoping — sixty minutes, no charge.