How to Give an AI Agent Safe Access to an Internal API with MCP

How to Give an AI Agent Safe Access to an Internal API with MCP


Our on-call team runs a small internal notes API. The web app in front of it displays who is holding the pager, which days are frozen for deploys, and where the runbooks live. A developer on that team wanted the same search from Cursor, without opening the web app.


They set it up the way most of us would on a Friday afternoon. They copied the API base URL into the assistant settings, pasted a long-lived token from the shared ops vault into an environment variable on their laptop, and added one line to the assistant instructions: “Only read notes. Do not change anything.”


On their machine it worked. They asked when the next deploy freeze was, the assistant called the search endpoint, and the right note came back.


It broke the following week, when a second developer pointed their own assistant at that same URL and that same token. The notes API cannot tell those two people apart.


Both requests arrive as the ops token, so the access log says the bot searched, not either developer. The assistant can also send a search string our API treats as “match everything,” because the server runs whatever text it receives. And because the notes process is open on the office network, anyone who knows the URL can call it directly and skip the sign-in screen the first developer saw in the assistant.


This tutorial rebuilds that setup in a form you can run on your laptop. You’ll wrap a small notes API in a Model Context Protocol (MCP) server, reject searches the assistant shouldn’t be able to write, and put a gateway in front so the assistant doesn’t call the notes process directly.


A later section explains how a real deployment ties each call to an account: the person who asked, or a service account for a job that runs overnight. The sample you run here stays open, so you don’t need a login provider.


By the end you’ll have:


  • A notes API with three functions: list, get, and search
  • An MCP server that exposes only those three tools
  • A gateway that enforces authentication, rate limiting, and request validation
  • A clear pattern for mapping every agent call to a real account, whether human or service

Why the Naive Setup Fails


The first attempt treated the assistant as a trusted client. That assumption breaks in three ways:


  1. Shared identity. Every user shares one long-lived token, so audit logs cannot distinguish between developers. When something goes wrong, you cannot trace the action to a person.
  2. Unrestricted input. The assistant can send arbitrary search strings. Without server-side validation, the API executes whatever it receives—including wildcards that return every note.
  3. Direct access. The internal API listens on the office network. Anyone with the URL can bypass the assistant and the sign-in screen entirely.

  4. MCP alone does not fix these problems. It standardizes how the assistant discovers and calls tools, but you still need to wrap the API, validate inputs, and place a gateway in front.


    Architecture Overview


    The safe setup has three layers:


    • Notes API — the existing internal service. Keep it unchanged.
    • MCP server — a thin wrapper that exposes only the operations the assistant needs and rejects anything else.
    • Gateway — the single entry point for the assistant. It handles authentication, rate limiting, and logging.

    The assistant never talks to the notes API directly. It talks to the gateway, which talks to the MCP server, which talks to the notes API.


    Step 1: Define the Notes API Surface


    Start by documenting the three functions you will expose. In a real system, these already exist. For this tutorial, assume a simple REST API:


    GET /notes
    GET /notes/{id}
    GET /notes/search?q={query}
    

    Each endpoint requires a bearer token. The search endpoint accepts a query string and returns matching notes.


    Step 2: Build the MCP Server


    The MCP server is where you enforce the tool contract. It should:


    • Expose exactly three tools: listnotes, getnote, and search_notes.
    • Validate every parameter before forwarding the request.
    • Reject any search query that contains wildcard characters or exceeds a maximum length.
    • Never expose raw HTTP methods like POST or DELETE.

    Here is a minimal Python implementation using the MCP SDK:


    from mcp.server import Server
    from mcp.types import Tool, TextContent
    import httpx
    import re
    
    app = Server("notes")
    
    NOTES_API = "http://localhost:8000"
    TOKEN = "internal-service-token"
    
    MAX_QUERY_LENGTH = 100
    WILDCARD_PATTERN = re.compile(r"[*?%_]")
    
    @app.list_tools()
    async def list_tools():
        return [
            Tool(name="list_notes", description="List all notes", inputSchema={"type": "object"}),
            Tool(name="get_note", description="Get a note by ID", inputSchema={
                "type": "object",
                "properties": {"id": {"type": "string"}},
                "required": ["id"]
            }),
            Tool(name="search_notes", description="Search notes", inputSchema={
                "type": "object",
                "properties": {"q": {"type": "string"}},
                "required": ["q"]
            }),
        ]
    
    @app.call_tool()
    async def call_tool(name: str, arguments: dict):
        async with httpx.AsyncClient() as client:
            headers = {"Authorization": f"Bearer {TOKEN}"}
            if name == "list_notes":
                r = await client.get(f"{NOTES_API}/notes", headers=headers)
            elif name == "get_note":
                r = await client.get(f"{NOTES_API}/notes/{arguments['id']}", headers=headers)
            elif name == "search_notes":
                q = arguments["q"]
                if len(q) > MAX_QUERY_LENGTH or WILDCARD_PATTERN.search(q):
                    return [TextContent(type="text", text="Invalid search query")]
                r = await client.get(f"{NOTES_API}/notes/search", params={"q": q}, headers=headers)
            else:
                return [TextContent(type="text", text="Unknown tool")]
        return [TextContent(type="text", text=r.text)]
    

    Run this server locally and point your assistant at it. The assistant now sees only the three tools, and the server rejects wildcard searches before they reach the notes API.


    Step 3: Add a Gateway


    The MCP server still trusts the assistant. A gateway in front adds a second layer of control.


    A gateway can:


    • Require a per-user token instead of a shared service token.
    • Rate-limit requests per user.
    • Log every tool call with the user identity attached.
    • Block requests that do not match the expected MCP protocol.

    You can use an existing API gateway (Kong, Apigee, AWS API Gateway) or a lightweight reverse proxy like Envoy. The key is that the assistant connects to the gateway, and the gateway forwards validated requests to the MCP server.


    For local testing, a simple FastAPI gateway works:


    from fastapi import FastAPI, Request, HTTPException
    import httpx
    
    app = FastAPI()
    MCP_SERVER = "http://localhost:3000"
    
    @app.post("/mcp")
    async def proxy(request: Request):
        auth = request.headers.get("Authorization")
        if not auth or not auth.startswith("Bearer "):
            raise HTTPException(status_code=401, detail="Missing token")
        body = await request.body()
        async with httpx.AsyncClient() as client:
            r = await client.post(MCP_SERVER, content=body, headers={"Content-Type": "application/json"})
        return r.json()
    

    In production, replace the static check with a real identity provider. Each developer authenticates and receives a short-lived token scoped to their account.


    Step 4: Map Every Call to an Account


    The sample above uses a shared service token because it runs on your laptop. In a real deployment, every call should carry an identity.


    There are two cases:


    • Human-initiated calls. The developer authenticates through your identity provider (Okta, Auth0, Google Workspace). The gateway validates the token and forwards the user ID to the MCP server. The MCP server includes that ID in every request to the notes API.
    • Service-initiated calls. A scheduled job or background agent uses a dedicated service account. The gateway validates the service account token and logs the job name instead of a human user.

    The notes API should trust only the MCP server, not the assistant or the gateway directly. This keeps the trust boundary small.


    Step 5: Test the Setup


    Run the notes API, the MCP server, and the gateway locally. Then:


    1. Ask the assistant to list notes. It should return the full list.
    2. Ask for a specific note by ID. It should return that note.
    3. Ask for a search with a wildcard like *. The MCP server should reject it.
    4. Try calling the notes API directly from your browser. It should reject the request because it requires the MCP server token.

    5. If all four pass, the setup is safe for local development.


      What This Does Not Cover


      This tutorial stays local and open. A production deployment needs:


      • A real identity provider for per-user tokens.
      • TLS between the assistant, gateway, and MCP server.
      • Secret management for service tokens (Vault, AWS Secrets Manager).
      • Centralized logging and alerting for anomalous tool calls.
      • A policy for which tools each user or role can access.

      Those are the next steps once the local pattern works.


      Conclusion


      Giving an AI agent safe access to an internal API is not about trusting the agent. It is about placing the agent behind the same controls you would apply to any other client: a narrow tool surface, input validation, a gateway, and a clear identity for every call.


      MCP gives you a standard way to expose tools. The rest is ordinary API security. Wrap the API, validate the input, and never let the assistant call the internal service directly. That is enough to turn a Friday-afternoon shortcut into something your on-call team can rely on.

      via FreeCodeCamp

Related