Insights & Blog

How to Build a Secure MCP Server for Enterprise APIs Using Python or TypeScript

Build a bounded MCP API adapter with Python, tenant-aware permission checks, five core tests and an enterprise authentication and deployment checklist.

By Hetal KGenerative AI & Agents

Secure MCP server implementation: validate input, authorize the principal, retrieve permitted records and return minimal fields.

How to Build a Secure MCP Server for Enterprise APIs Using Python or TypeScript

To build a secure MCP server for an enterprise API, start with a small business capability, enforce permissions before calling the API and return only the fields the caller needs. Use an official SDK for protocol handling. Keep credentials and authorization decisions outside model-generated arguments, and test rejected requests as carefully as successful ones.

This guide provides a Python implementation of a fictional tenant-isolated ticket lookup. The core authorization function and its tests run without a network connection. An MCP adapter exposes that function locally. A separate deployment checklist explains what remains before connecting a real API or exposing a remote endpoint. The example is deliberately read-only: it is not a production authentication system or a claim that installing an SDK secures an integration.

Implementation decisions at a glance

  • Use a fixed tool contract such as get_ticket_summary; do not accept arbitrary URLs, SQL or shell commands.
  • Derive the principal from a trusted runtime or validated authentication, never a tool parameter.
  • Separate the permission check from protocol code so it can be tested directly.
  • Use a consistent external response for records that are missing or inaccessible where existence itself is sensitive.
  • For remote deployment, implement the applicable MCP authorization flow and verify the actual host/SDK combination.

1. Define a bounded enterprise API capability

Suppose a support assistant needs to summarize the status of a ticket for an authorized specialist. The tool needs a ticket identifier. It does not need a destination URL, an access token, an arbitrary tenant identifier or a query language. Those additional arguments would let a model propose authority or destinations that should be established by trusted code.

Contract elementIllustrative decisionWhy it matters
InputA ticket ID in the format T-1234A small validation surface
IdentityA principal supplied by the runtimeThe model cannot select another user
PermissionTicket-read scope and matching tenantBoth operation and record boundary are checked
OutputID and status onlyContact details and internal notes are unnecessary
FailureUnavailable for missing or inaccessible recordsThe lookup need not reveal another tenant’s records

For the protocol roles and overall request path, read the MCP architecture guide. Here the focus is the executable boundary inside the server.

2. Implement permission checks independently of MCP

Save the following as ticket_service.py. All identities and records are fictional. In this example a frozen principal contains the already-established tenant and scopes; it is not a substitute for verifying a real credential.

import re
from dataclasses import dataclass

@dataclass(frozen=True)
class Principal:
    tenant: str
    scopes: frozenset[str]

RECORDS = {
    "T-1042": {"tenant": "alpha", "status": "awaiting review"},
    "T-2042": {"tenant": "beta", "status": "in progress"},
}

def lookup_ticket(principal: Principal, ticket_id: str) -> dict:
    if not re.fullmatch(r"T-[0-9]{4}", ticket_id):
        raise ValueError("Invalid ticket ID")
    if "tickets:read" not in principal.scopes:
        raise PermissionError("Read permission required")
    record = RECORDS.get(ticket_id)
    if record is None or record["tenant"] != principal.tenant:
        return {"available": False}
    return {"available": True, "id": ticket_id,
            "status": record["status"]}

The function checks the operation permission before exposing a record and checks tenant ownership before returning fields. Returning a newly constructed dictionary also prevents accidental exposure of the complete internal record. A production implementation may need more granular rules than tenant equality, including record sharing, team membership or purpose restrictions. Keep those rules in the authoritative business service when it owns them.

3. Expose the function through an MCP adapter

Save this as server.py next to the core file. The local adapter uses a fixed fictional principal so that the example is reproducible without credentials. Never expose this fixed identity as a shared remote service.

from mcp.server.fastmcp import FastMCP
from ticket_service import Principal, lookup_ticket

mcp = FastMCP("Tenant ticket lookup demo")
DEMO_PRINCIPAL = Principal("alpha", frozenset({"tickets:read"}))

@mcp.tool()
def get_ticket_summary(ticket_id: str) -> dict:
    """Read a permitted fictional ticket's ID and status."""
    return lookup_ticket(DEMO_PRINCIPAL, ticket_id)

if __name__ == "__main__":
    mcp.run(transport="stdio")

Create an isolated Python environment and install a compatible official mcp SDK version. Record the resolved version in the project lock file. Configure the host to launch that environment’s interpreter with the absolute path to server.py. Follow the official server tutorial and Python SDK documentation for your selected release’s installation and host configuration.

Protocol traffic uses standard output in this local transport. Send ordinary diagnostics to standard error. If tool discovery fails, first check the interpreter path, environment dependencies and process errors before changing the authorization function.

What changes for a remote endpoint?

A remote service needs real authentication and request-scoped identity. Replace the demonstration principal with identity derived from validated authorization context, then carry that context to the business permission check. Do not simply accept tenant_id or scopes from the tool call. A transport change alone does not implement this boundary.

The MCP authorization specification describes authorization for HTTP-based transports. Use its supported discovery and token-validation behavior through a reviewed implementation. Distinguish an access token intended for the MCP server from credentials needed by a downstream business API; do not blindly forward an incoming bearer token to another service.

4. Test denied access, missing records and invalid input

Save this as test_ticket_service.py. These standard-library tests exercise the core rules without starting an MCP host.

import unittest
from ticket_service import Principal, lookup_ticket

class TicketAccessTests(unittest.TestCase):
    def setUp(self):
        self.reader = Principal("alpha", frozenset({"tickets:read"}))

    def test_allowed_record(self):
        self.assertEqual(lookup_ticket(self.reader, "T-1042"),
                         {"available": True, "id": "T-1042",
                          "status": "awaiting review"})

    def test_other_tenant_is_unavailable(self):
        self.assertEqual(lookup_ticket(self.reader, "T-2042"),
                         {"available": False})

    def test_missing_record_is_unavailable(self):
        self.assertEqual(lookup_ticket(self.reader, "T-9999"),
                         {"available": False})

    def test_scope_is_required(self):
        with self.assertRaises(PermissionError):
            lookup_ticket(Principal("alpha", frozenset()), "T-1042")

    def test_invalid_identifier(self):
        with self.assertRaises(ValueError):
            lookup_ticket(self.reader, "https://example.invalid/")

if __name__ == "__main__":
    unittest.main()

Run python -m unittest -v test_ticket_service.py from the example directory. The five core tests passed during preparation of this article. That result covers the in-memory permission logic only. It does not validate OAuth, a remote transport, an MCP host, a real enterprise API or production tenant isolation.

After attaching the MCP adapter to a host, repeat the same cases through the protocol. Also test malformed argument types, interrupted connections, a host reconnect and a changed tool definition. A successful direct function test should not be mistaken for a successful end-to-end integration test.

5. Replace the demonstration store with a real API

Keep the tool signature narrow when replacing the store. Configure an approved upstream base address server-side, use a supported client and construct the record path from validated identifiers. Do not let a tool argument override the API host or choose a redirect destination.

Decide explicitly whether the API request uses delegated user authority or a service identity. If a service identity has broad access, the adapter still needs an enforceable record-level authorization path. An application’s successful login does not prove the caller can access every record the service credential can retrieve.

Handle upstream failures deliberately

  • Timeout: stop within an agreed deadline and report that current information could not be retrieved.
  • Permission failure: deny the lookup rather than retry with a broader identity.
  • Rate limit: apply bounded backoff according to the upstream contract; avoid synchronized retry storms.
  • Unexpected response: validate the expected schema and reject malformed data.
  • Partial information: identify what is missing rather than present a complete-looking summary.

Even read operations can create load or trigger upstream behavior, so confirm retry semantics with the API owner. For future write capabilities, add idempotency and explicit approval requirements before enabling automatic retries.

6. Deployment checklist for an enterprise service

The local example becomes a deployable service only after the surrounding controls are implemented. Treat the following as acceptance criteria to review with the identity, business API and operations owners.

AreaRequired evidence
IdentityInvalid, expired and wrong-audience credentials are rejected; caller context cannot be replaced by a tool argument
AuthorizationCross-tenant, unshared-record and revoked-access cases fail through the full request path
NetworkingOnly intended ingress and upstream destinations are reachable
SecretsCredentials are managed outside prompts and source files; ownership and rotation are defined
ReliabilityTimeouts, concurrency, upstream rate limits and recovery behavior are tested
AuditCorrelation IDs and authorization outcomes are available without logging access tokens or unnecessary record contents
ReleaseDependencies and tool contracts are versioned; compatibility tests and rollback are available

Apply a content trust boundary too. Retrieved text may be relevant business data while containing malicious instructions. It cannot grant permission to call other tools or change the deployment configuration. Review the official MCP security guidance alongside the organization’s existing API security controls.

Troubleshooting common implementation failures

Works locally, fails in the host: verify the host launches the same interpreter and environment. Tools are discoverable but calls fail: compare the published input schema with the actual arguments. Another tenant’s record appears: stop rollout and inspect the trusted principal and record permission path. Logs contain secrets: restrict access, follow the credential incident process and correct logging before resuming.

For a Python-versus-TypeScript decision, choose the language your team can operate and test reliably, then confirm the required authentication and transport support in its official SDK. A familiar runtime with strong tests is more useful than selecting a language because an example is shorter.

Common questions about building an MCP server

How do you deploy an MCP server for business applications?
Define narrow capabilities, implement caller and record authorization, connect the supported business API and validate the chosen host/SDK combination. A remote deployment also needs the applicable authentication flow, network restrictions, monitoring and release controls.
Can I pass the tenant ID as a tool argument?
A tenant identifier may identify requested data, but it must not establish authority. Derive the caller’s permitted tenants from trusted identity and verify any requested tenant against that permission.
Is a local stdio server automatically secure?
No. It runs with the access available to its process. Review its executable, dependencies, credentials, local file access and host configuration. The example here intentionally uses only fictional in-memory records.
Should the MCP server return the complete API response?
Return the fields required by the capability. Filtering helps avoid unnecessary disclosure and makes the tool contract more stable when the upstream response changes.
Are the tutorial’s tests sufficient for production?
No. They verify five core function behaviors. A production release also needs protocol, identity, upstream API, operational and tenant-isolation tests in the actual environment.

Scope an integration that your team can operate

Discuss an AI agent integration assessment with Prominent AI Services LLP. Bring the intended capability, API owner, permission model and expected failure cases so the implementation starts with a reviewable boundary.

Technical references reviewed 2 November 2026. Demonstration records are fictional; no customer system was accessed for the example.

From insight to implementation

Turn these ideas into a practical plan

Discuss how the ideas in this article apply to your business, systems and data. We’ll help you identify a focused first step.