AI Agent Engineering From Scratch
- AI Agents Part 1: Architecture
- AI Agents Part 2: Harness and CLI
- AI Agents Part 3: Tools and APIs
- AI Agents Part 4: MCP
- AI Agents Part 5: Context and Memory
- AI Agents Part 6: Agentic RAG
- AI Agents Part 7: Evals and Observability
- AI Agents Part 8: Security and Deployment
Part 3 explained how an agent calls tools. Those tools may be part of the same application as the agent.
MCP helps when several AI applications need access to the same external service.
MCP stands for Model Context Protocol. It defines how an AI application exchanges structured messages with an external service.
For example, one MCP server can provide access to a calendar. Several AI applications can use that server without separate calendar adapters.
Place MCP in the stack
Three components matter:
- The host is the AI application that the user runs.
- The client is the part of the host that sends MCP requests.
- The server receives MCP requests and provides tools, content, or message templates.
The host contains the MCP client. The model asks the host to use a tool.
A typical request moves like this:
user
-> AI host
-> model selects an available tool
-> MCP client sends a protocol request
-> MCP server calls a local function or business API
-> result returns to the host
-> host sends the tool result to the model
The host chooses which servers to connect and which tools to show the model. It also selects the user identity and approval rules.
MCP defines the messages that pass between the client and server. The host still controls permissions, approvals, and model access.
The three types an MCP server can provide
MCP servers can provide tools, resources, and prompts. Each type has a different purpose.
Tools
A tool is a named operation that the model can request. The host decides whether to run it.
Examples include searching a ticket system, reading a database record, running a report, or creating a draft. Tools accept structured arguments and return content or structured results.
The relevant protocol operations include tools/list and tools/call. The current normative specification is available on the MCP page for tools.
A write tool still needs application authorization and approval. Calling it through MCP does not make the action safe.
Resources
A resource is content with a stable identifier. The client uses that identifier to read the content.
Examples include a document, schema, repository file, policy version, or generated artifact.
A client can list or read resources. Reading a resource does not run a tool.
Resources often use URI-like identifiers:
policy://returns/2026-08
repo://project/src/config.ts
report://monthly/2026-07
Stable identifiers help with citations, caching, access control, and traces. See the current specification for resources.
Prompts
A prompt is a reusable message template. The host or user selects it.
Prompts are useful for tasks such as "review this change" or "prepare an incident summary."
The application must enforce its security policy outside the prompt. The host may display, modify, or combine prompts.
The protocol operations include prompts/list and prompts/get. See the specification for prompts.
Choosing the right primitive makes the interface easier to understand. A document is usually a resource. A query or mutation is usually a tool. A reusable starting conversation is a prompt.
MCP and function calling solve different problems
Function calling lets a model request a tool from its host. MCP lets the host send that request to an MCP server.
A host may translate MCP tools into the provider-specific tool format sent to the model. When the model requests one, the host translates the request back into tools/call.
That means both layers can exist in the same run:
model function call
-> host policy and validation
-> MCP tools/call
-> server implementation
-> REST API or local function
Keep provider-specific model formats at the model boundary. Keep MCP protocol behavior in the client and server adapters. Keep business rules in the underlying service.
Follow one request through the complete stack
Consider a support agent that can inspect an order and draft a refund. The order service already has two REST endpoints:
GET /orders/{order_id}
POST /refund-drafts
The team wants several AI hosts to use the same operations. It adds an MCP server in front of the existing service.
The server advertises two tools. The read tool returns the current order state. The write tool creates a draft but does not issue money.
{
"name": "get_order",
"description": "Read the current status and refundable amount for one order.",
"inputSchema": {
"type": "object",
"properties": {
"order_id": { "type": "string", "pattern": "^ord_[A-Za-z0-9]+$" }
},
"required": ["order_id"],
"additionalProperties": false
},
"outputSchema": {
"type": "object",
"properties": {
"order_id": { "type": "string" },
"status": { "type": "string" },
"currency": { "type": "string" },
"refundable_amount_minor": { "type": "integer", "minimum": 0 },
"policy_version": { "type": "string" }
},
"required": [
"order_id",
"status",
"currency",
"refundable_amount_minor",
"policy_version"
],
"additionalProperties": false
}
}
The host gives the model this schema through its normal tool interface. The model requests get_order with ord_4821.
The host does not send the request immediately. It first checks that the signed-in user can read this account. It then validates the argument and records the pending call.
For Streamable HTTP, the MCP client sends a self-describing tools/call request:
POST /mcp HTTP/1.1
MCP-Protocol-Version: 2026-07-28
Mcp-Method: tools/call
Mcp-Name: get_order
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 41,
"method": "tools/call",
"params": {
"name": "get_order",
"arguments": { "order_id": "ord_4821 },
"_meta": {
"io.modelcontextprotocol/clientInfo": {
"name": "refund-agent",
"version": "1.0"
}
}
}
}
With stdio, the same protocol information travels in the message metadata instead of HTTP routing headers.
The MCP server maps that call to GET /orders/ord_4821. It uses a delegated user token, token exchange, or signed principal context.
The protected service validates the issuer, audience, subject, scope, and requested resource. It does not trust scope text supplied by the model.
The order service remains responsible for the business decision. It checks account ownership and returns the current order record.
The MCP server returns a JSON-RPC response with a small structured result:
{
"jsonrpc": "2.0",
"id": 41,
"result": {
"structuredContent": {
"order_id": "ord_4821",
"status": "delivered_late",
"currency": "EUR",
"refundable_amount_minor": 4250,
"policy_version": "returns-2026-08"
}
}
}
The server validates structuredContent against the tool's output schema before returning it.
The host records the result and gives it to the model. The model can now propose a refund draft with the verified amount and policy version.
This trace shows the ownership of each decision:
| Layer | Decision |
|---|---|
| Model | Which available tool could advance the task? |
| Host | May this model request reach this server for this user? |
| MCP client | How should the host encode and send the protocol request? |
| MCP server | Which service operation implements the MCP tool? |
| Order service | Is the operation valid under current business and access rules? |
Do not collapse these decisions into one prompt. Each layer has information that the other layers should not own.
Design results for the next decision
A tool result becomes context for the model. Return the fields needed for its next decision, not the complete upstream response.
The order API might include warehouse notes, customer addresses, internal fraud scores, and dozens of timestamps. The refund step needs none of them.
Shape the MCP result at the server boundary. This reduces accidental disclosure and makes tool behavior easier to test.
Return stable error categories too:
{
"error": {
"code": "ORDER_NOT_REFUNDABLE",
"message": "This order has no refundable amount.",
"retryable": false
}
}
The host can handle this result without asking the model to infer whether another identical call might work.
MCP works with your existing API
REST, GraphQL, queues, and direct functions remain useful.
A business API defines stable operations for many clients. An MCP server can expose selected API operations to AI applications.
The MCP server can call the existing API underneath.
This separation has several advantages:
- web and mobile clients keep using the normal API.
- the MCP adapter returns model-friendly schemas and smaller results.
- credentials and network policy stay on the server side.
- business rules remain independent of the AI host.
- several hosts can reuse the same integration.
Do not move core authorization or accounting logic into an MCP description. Keep it in the service that owns the resource.
Choose how to carry MCP messages
A transport carries MCP messages between the client and server. MCP commonly uses two transports.
stdio
The stdio transport sends messages through a local process. The host starts that process and uses standard input and standard output.
Use it for:
- desktop applications.
- local developer tools.
- coding agents.
- processes that should inherit a controlled workspace.
- integrations that do not need a network service.
Standard output belongs to the protocol. Send logs to standard error so an ordinary log line does not corrupt the message stream.
The host controls process lifetime, environment variables, working directory, and local permissions. That control is useful, but a local process can still access more of the machine than intended. Use operating-system isolation where the risk requires it.
Streamable HTTP
Streamable HTTP sends MCP messages over HTTP. Use it when several clients must connect to a remote server.
It fits enterprise connectors, centrally managed integrations, and servers that need independent scaling.
This network connection needs TLS, request limits, authentication, authorization, and audit logs.
The server must also prevent server-side request forgery, or SSRF. An SSRF attack makes the server connect to a forbidden network address.
The transport affects process isolation, network controls, authentication, and daily operations.
Understand the current stateless core
In the 2026-07-28 revision, each request contains the information needed to process it. The server does not depend on a transport session.
Earlier versions used an initialization exchange and a transport session identifier.
A client may use server/discover to learn supported versions and capabilities. Discovery is optional. Clients can send a request directly and handle an unsupported-version response.
Applications can still store explicit state. A server can return a task or resource ID, and the client can use that ID later.
This state appears in the application contract instead of a transport session.
The official 2026-07-28 release notes explain these changes.
Authenticate and authorize each request
Authentication confirms the caller's identity. Authorization determines what the caller may do.
For remote HTTP servers, use tokens created for that server. Do not copy one broad token through several services.
The MCP authorization specification uses OAuth resource indicators. A resource indicator identifies the server that should accept a token.
The server also publishes metadata. This metadata tells the client which authorization server to use.
The host still needs to:
- confirm who issued the token and which server may accept it.
- keep the authenticated user or service identity.
- expose only allowed capabilities.
- validate arguments.
- ask for approval where required.
- restrict downstream credentials.
- record the decision and result.
Descriptions such as readOnlyHint can improve the interface. Enforce the related policy in the host and protected service.
A server can provide incorrect or hostile descriptions.
Treat returned content as untrusted input. It can contain instructions that try to make the model ignore its rules.
This attack is called prompt injection.
When to use each boundary
| Boundary | Best fit |
|---|---|
| Direct function | One process, one codebase, no interoperability need |
| CLI | Scriptable human and automation interface |
| REST or GraphQL | Stable network service for many client types |
| Function calling | Structured model-to-host requests |
| MCP | Reusable AI-host-to-capability integration |
One integration may use several of these options at different layers.
Use MCP when several AI applications need the same integration. A custom interface is often sufficient inside one application.
Adding MCP to every internal function increases code, testing, and security work.
Plan for protocol and service failures
An MCP call crosses several boundaries, so a generic tool failed message is not enough. The host needs to know what failed and whether another attempt is safe.
Common failure modes include:
| Failure | Correct owner | Typical response |
|---|---|---|
| Invalid tool arguments | Host and server | Reject before calling the business service |
| Expired or wrong-audience token | MCP server | Return an authentication error and refresh through the approved flow |
| User lacks access | Protected service | Deny without revealing the protected record |
| Server unavailable | Host | Retry only when the operation and policy allow it |
| Upstream timeout after a write | Business service | Resolve with an idempotency key or operation-status lookup |
| Malformed protocol response | MCP client | Reject the response and record the server version |
| Hostile returned text | Host | Keep it in the untrusted result channel |
| Tool schema changed | Release process | Detect the change before exposing it to production models |
A read call can often tolerate a bounded retry. A write call needs stronger evidence.
Suppose create_refund_draft times out after the server sends the REST request. The host cannot know whether the draft exists.
Sending the call again without an idempotency key can create two drafts. The MCP arguments should include a stable operation key. The server can also derive one from the run and action IDs.
The business service must enforce that key. A prompt instruction to "avoid duplicates" cannot provide this guarantee.
Version changes also need deliberate handling. Record the protocol version, server release, tool name, and schema hash in each trace.
When a tool changes, test old valid calls, new valid calls, missing fields, additional fields, and wrong field types. Do this before the host advertises the new schema.
Build the smallest useful MCP adapter
Use this order for a first implementation:
- Choose one existing service operation with a clear owner.
- Decide whether it belongs as a tool, resource, or prompt.
- Write the smallest input schema that expresses the operation.
- Define a result schema for the model's next decision.
- Define stable error categories and retry behavior.
- Map the MCP operation to the existing API or function.
- Carry user or service identity to the protected service.
- Add host-side approval for actions that need it.
- Record requests, policy decisions, results, versions, and timing.
- Test denied, malformed, duplicate, delayed, and hostile inputs.
Start with one read operation. Add a write operation after the read path has clear traces, identity propagation, and predictable errors.
The adapter is ready for another host when that host can discover the operation and validate its schema. It must also enforce access and interpret each result.
Review an MCP integration before connecting it
Ask:
- Which tools, resources, and prompts does the server expose?
- Which of them can change state?
- Which user or service identity does the server receive?
- What network destinations can the server reach?
- Which credentials does it hold?
- Can returned content influence later tool calls?
- How are versions pinned?
- Which requests, approvals, and results appear in the audit log?
- How can access be revoked?
- What happens when the server is unavailable or compromised?
MCP Inspector is a development tool that checks protocol messages.
Use separate tests for authorization, unsafe input, and model tool selection.
Previous: AI Agents Part 3: Tools and APIs
Next: AI Agents Part 5: Context and Memory explains how the host decides what information the model receives on each turn.