<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Pixel Web Solutions Engineering]]></title><description><![CDATA[Pixel Web Solutions Engineering]]></description><link>https://pixelwebsolutions.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Fri, 30 Oct 2026 06:26:27 GMT</lastBuildDate><atom:link href="https://pixelwebsolutions.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Designing an MCP Server for an Internal AI Agent: Tools, Auth and Guardrails]]></title><description><![CDATA[How to design an MCP server for an internal AI agent: one tool per business action, OAuth 2.1 resource server auth, and guardrails enforced in code.]]></description><link>https://pixelwebsolutions.hashnode.dev/designing-an-mcp-server-for-an-internal-ai-agent-tools-auth-and-guardrails</link><guid isPermaLink="true">https://pixelwebsolutions.hashnode.dev/designing-an-mcp-server-for-an-internal-ai-agent-tools-auth-and-guardrails</guid><category><![CDATA[mcp]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[oauth]]></category><category><![CDATA[Security]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Pixel Web Solutions]]></dc:creator><pubDate>Mon, 26 Oct 2026 11:24:07 GMT</pubDate><content:encoded><![CDATA[<p>Most internal AI agents need to do more than chat. They need to look up an order or open a ticket. The Model Context Protocol (MCP) has become the standard way to expose those capabilities: you run an MCP server in front of your systems, and any MCP-capable agent can discover and call its tools.</p>
<p>This article walks through how we design an MCP server for an internal agent. It is based on the current specification, version 2026-07-28, and focuses on three things that decide whether a server is safe to put into production: tool design, authorization and guardrails.</p>
<h2>The shape of an MCP server</h2>
<p>MCP uses JSON-RPC 2.0 messages between a host (the AI application), a client inside that host, and your server. A server can offer three kinds of features: resources (data the model can read), prompts (templates for users) and tools (functions the model can call).</p>
<p>For an internal agent, tools do most of the work. The client calls <code>tools/list</code> to discover them and <code>tools/call</code> to run one.</p>
<p>One change in the current spec matters for design. Requests are stateless and self-contained, and there is no protocol-level session. If a tool needs to keep state across calls, such as a draft refund, it should return an explicit handle that later calls pass back in.</p>
<h2>Step 1: Design tools around business actions</h2>
<p>The most common mistake is wrapping an entire internal API as one generic tool, such as <code>call_api(path, body)</code>. The model has to guess endpoints, and you cannot apply different rules to different actions.</p>
<p>Instead, design one tool per business action, with a narrow, typed input:</p>
<pre><code class="language-json">{
  "name": "orders.get_status",
  "title": "Get order status",
  "description": "Return status, carrier and last update for one order. Read only.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "order_id": { "type": "string", "pattern": "^ORD-[0-9]{8}$" }
    },
    "required": ["order_id"],
    "additionalProperties": false
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "status": { "type": "string" },
      "carrier": { "type": "string" },
      "updated_at": { "type": "string" }
    },
    "required": ["status", "updated_at"]
  }
}
</code></pre>
<p>A few rules we follow:</p>
<ul>
<li><strong>Name tools by domain and verb.</strong> The spec recommends names of 1 to 128 characters using letters, digits, underscore, hyphen and dot. <code>orders.get_status</code> and <code>tickets.create_draft</code> read clearly in logs.</li>
<li><strong>Split reads from writes.</strong> <code>refunds.draft</code> and <code>refunds.submit</code> should be separate tools so they can carry different permissions and approval rules.</li>
<li><strong>Use an output schema.</strong> When a tool declares one, the server must return structured content that matches it, and clients should validate it. That makes results predictable for the model and for your tests.</li>
<li><strong>Write descriptions for the model.</strong> Say what the tool does and any limits, such as "drafts expire after 24 hours".</li>
</ul>
<h2>Step 2: Put real authorization in front of it</h2>
<p>If your server runs over HTTP, treat it as an OAuth 2.1 resource server, which is how the <a href="https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization">MCP authorization specification</a> defines it. The important requirements are:</p>
<ul>
<li>The server <strong>must</strong> publish OAuth 2.0 Protected Resource Metadata (RFC 9728) so clients can discover which authorization server to use.</li>
<li>Clients <strong>must</strong> send a <code>resource</code> parameter (RFC 8707) naming your server, and the server <strong>must</strong> check that each token was issued for it as the audience.</li>
<li>The server <strong>must not</strong> accept or pass through tokens meant for other services. If a tool calls a downstream API, it gets its own credential for that API.</li>
<li>Invalid or expired tokens get a 401. Missing scopes get a 403 with an <code>insufficient_scope</code> challenge.</li>
</ul>
<p>An unauthenticated request receives a challenge that points the client at your metadata:</p>
<pre><code class="language-http">HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer resource_metadata="https://mcp.internal.example.com/.well-known/oauth-protected-resource",
                         scope="orders:read"
</code></pre>
<p>Map scopes to tool groups, not to individual endpoints. A simple scheme is <code>orders:read</code>, <code>tickets:write</code> and <code>refunds:submit</code>. The spec also allows the tool list to vary by the authorization on the request, so a caller with only <code>orders:read</code> can be shown only the read tools.</p>
<p>If you run the server locally over STDIO instead, the spec says not to use this OAuth flow. Read credentials from the environment and keep the server's own permissions narrow.</p>
<h2>Step 3: Enforce guardrails in the server, not the prompt</h2>
<p>Injected text can override prompts, so guardrails have to live in code. The <a href="https://modelcontextprotocol.io/specification/2026-07-28/server/tools">tools section of the spec</a> says servers must validate all tool inputs, implement access controls, rate limit tool invocations and sanitize tool outputs. Here is the handler pattern we use, written in plain TypeScript so it fits any SDK:</p>
<pre><code class="language-ts">async function handleToolCall(req: ToolRequest, auth: VerifiedToken) {
  const tool = registry.get(req.name);
  if (!tool) return protocolError(-32602, `Unknown tool: ${req.name}`);

  if (!auth.scopes.includes(tool.requiredScope)) {
    return scopeChallenge(tool.requiredScope); // HTTP 403 insufficient_scope
  }

  const input = tool.schema.safeParse(req.arguments);
  if (!input.success) {
    return toolError(`Invalid input: ${input.error.message}`); // isError: true
  }

  await rateLimiter.consume(auth.subject, tool.name);

  if (tool.requiresApproval) {
    return toolError("This action needs human approval. A draft was created instead.");
  }

  const result = await tool.run(input.data, auth);
  await audit.log({ user: auth.subject, tool: tool.name, input: input.data, ok: true });
  return ok(redact(result));
}
</code></pre>
<p>Three details are worth calling out:</p>
<ul>
<li><strong>Validation errors are tool results, not crashes.</strong> The spec separates protocol errors (unknown tool, malformed request) from tool execution errors, which come back with <code>isError: true</code>. Returning a clear message lets the model correct itself and retry.</li>
<li><strong>Redact before returning.</strong> Strip fields the model does not need, such as full card numbers or internal notes. Whatever you return can end up in a model's context and in its answer.</li>
<li><strong>Log every call.</strong> Record who called which tool, with what input and outcome. These logs are your audit trail.</li>
</ul>
<h2>Step 4: Keep a human in the loop for risky actions</h2>
<p>The spec says there should always be a human in the loop with the ability to deny tool invocations, and that clients should ask for confirmation on sensitive operations. Do not rely on the client alone. Mark high-risk tools on the server side and make them produce drafts that a person approves in your own system.</p>
<p>Also remember that tool annotations, the hints that describe a tool as read-only or destructive, are to be treated as untrusted unless they come from a trusted server. That is another reason for the server to enforce its own rules.</p>
<h2>Step 5: Handle state with explicit handles</h2>
<p>Because there is no session, multi-step work needs handles. A <code>refunds.draft</code> tool returns a <code>draft_id</code>, and <code>refunds.submit</code> takes that ID. On every call, check that the caller is allowed to act on that handle, make IDs opaque and random, and state the expiry in the tool description so the model knows when to start again.</p>
<h2>A production checklist</h2>
<ul>
<li>One tool per business action, with strict input and output schemas.</li>
<li>Protected Resource Metadata published, audience checked on every token, no token passthrough.</li>
<li>Scopes mapped to tool groups, with read and write separated.</li>
<li>Input validation, rate limits, output redaction and audit logs in the handler.</li>
<li>Human approval for anything that moves money, deletes data or contacts a customer.</li>
<li>An evaluation set of real requests run against the server before each release.</li>
</ul>
<h2>Further reading</h2>
<p>We cover cost, hosting and the 2026 spec changes in more depth in <a href="https://www.pixelwebsolutions.com/mcp-server-development/">Pixel's MCP server development guide</a>. If you are still deciding whether your agent needs tools at all, or just better retrieval, our <a href="https://www.pixelwebsolutions.com/rag-vs-mcp/">comparison of RAG and MCP</a> has a decision table. For the wider threat model, the <a href="https://www.pixelwebsolutions.com/ai-agent-security/">Pixel Web Solutions</a> write-up on agent security lists the controls we apply on client projects.</p>
<p>Written by the AI engineering team at <a href="https://www.pixelwebsolutions.com/">Pixel Web Solutions</a>, headquartered in Madurai, India, with an office in Ras Al Khaimah, UAE.</p>
]]></content:encoded></item></channel></rss>