Essay · 45 min read

MCP for enterprise AI: a practical guide to integration, authority, and operations

Quick brief

MCP standardizes how AI applications discover and use tools, resources and prompts, while the surrounding system must enforce identity, authority and execution controls. This guide builds a bounded contract-evidence service and client, then examines protocol versions, deployment choices, failure tests, security boundaries and the cost of a correctly completed task before extending the design to consequential actions.

Listen to the AI-generated overview · about 2 minutes

AI-generated overview of Ajith Vallath Prabhakar’s article. Contract C-204 and its associated records are fictional. Approximately 1 minute 43 seconds.
Read the audio transcript

Timestamps are approximate.

0:00

This is an AI-generated overview of the revised article on the Model Context Protocol.

0:06

An assistant can explain a contract renewal, but completing the work means reaching the right records and tools. The Model Context Protocol, or MCP, gives applications a common way to discover and call server capabilities. The interface alone does not settle which identity may use a capability, or whether a business action should proceed.

0:28

Take the guide’s fictional contract C-204. An assistant reads renewal terms and an approval record, then recommends a next step. Read access does not authorize renewal. The recommendation should show its evidence and its limits. The contract remains unchanged until separate action authority is established, and the destination confirms the result. MCP standardizes an interaction; the surrounding application still owns the decision controls.

0:55

An MCP host manages client connections, and servers expose capabilities, such as tools, resources, and prompts. The article’s small Python example uses a local stdio connection to retrieve contract evidence. Its focused tests do not establish network transport, OAuth, business authorization enforcement, or production readiness.

1:18

A practical implementation starts with one bounded operation. Define the identity, permitted action, evidence source, freshness needs, and failure behavior. Test denial and uncertainty as well as success, then measure the complete, correctly finished task. A direct connection may be simpler for one service; a gateway can help coordinate shared controls. Neither choice supplies business authority by itself.

Full analysis

Revised October 18, 2026.

An assistant can explain a contract renewal but cannot help finish the work until it can reach the relevant records and tools. Connecting it to those systems is an integration problem. Deciding what it may do through the connection is an authority problem. The Model Context Protocol, or MCP, gives applications a common interface for tools and context; the service around that interface still has to address both.

Suppose an assistant retrieves contract C-204, its renewal terms, and the approval record, then recommends renewal from the evidence it received. The approval permits reading the contract, so the proposed renewal must remain on hold until separate action authority is established. A useful system can present that recommendation and its limit together. If it reports the contract as renewed because a tool request succeeded, the integration has concealed the most important fact.

C-204 and every associated identifier, record, permission, and event in this guide are fictional. I use this small example because it makes the design concrete: we can name the operation, inspect its inputs, and ask what evidence would justify its result. I would begin an enterprise MCP project at that scale before connecting a catalogue of business operations.

Fictitious contract C-204 workflow: an MCP connection returns renewal evidence, while a proposed renewal is held because the approval scope is read only. A renewed contract requires separate action authorization and destination confirmation.
Fictitious contract-renewal example; the contract identifier and all details are illustrative. Read access permits retrieval of renewal evidence, not renewal of the contract. The proposed renewal is held; the inset shows a separate outcome possible only after valid action-specific authorization and destination confirmation. Open the full-size illustration.

Executive takeaway

MCP is worth considering when several AI applications need reusable access to the same capabilities. It standardizes their conversation with a server, while identity, business permissions, source validity, and recovery remain design responsibilities. A direct API integration can still be the simpler choice for one fixed workflow.

Start with a bounded read operation and prove its behavior without a model. Then connect the intended host, test denial and failure cases, and measure the complete task. Add write operations only when approval scope, execution conditions, and destination confirmation can be checked independently.

Where a common interface reduces work

The usual integration argument compares M applications and N business capabilities. If every application has its own connector to every capability, the diagram contains M × N pairings. With a common protocol, the application side can share an MCP client implementation and each capability can expose an MCP server interface. That is the intuition behind the M + N comparison.

For four applications and six systems, the two diagrams contain 24 pairings and 10 participating implementations respectively. Those numbers describe an interface arrangement, not a measured reduction from 24 units of engineering to 10. Each business system still has its own data model, permissions, failure behavior, and operating owner. A shared client also needs compatibility testing against the servers it uses.

The benefit becomes tangible when a second application can discover and call an existing, well-defined contract lookup without rebuilding its protocol adapter. The lookup still needs to understand what “contract” means, which legal entity owns it, and which fields the caller may see. MCP gives that service a reusable entrance; it does not supply those business definitions.

There is a reasonable case against adding it. If one deterministic application calls one stable API, another protocol layer may add deployment work without enough reuse to justify it. Keep the direct integration when it is easier to secure, operate, and maintain. I would choose MCP for a clear interoperability benefit, rather than making protocol adoption a project outcome in its own right.

Function calling and MCP also sit at different points in the system. A model API can return a proposed function name and arguments. The host decides how to handle that proposal; an MCP client is one way to reach the implementation. A workflow can use both, and a deterministic program can call an MCP tool without an LLM. The distinction matters during testing because the tool service can be verified before introducing model selection and interpretation.

Follow responsibility through the host, client, and server

The host is the application the reader or operator uses, whether an assistant, an IDE, or a custom workflow. It manages model interaction and decides which capabilities to expose for the task through its MCP clients. Each client communicates with a server that exposes capabilities and connects them to the underlying system; one host can manage several such clients. These roles are defined in the MCP architecture specification.

For C-204, the host responds to a renewal-review request by calling a contract-evidence server through its client. That server reads the permitted record from the contract service, while a separate server might expose an approved renewal workflow. Sharing an interface does not make the two operations equivalent: one retrieves evidence; the other could create an obligation.

ComponentResponsibility in the exampleFailure to prevent
HostKeep the requested task, selected tools, approval presentation, and final explanation alignedA request to review a renewal silently becomes a request to execute it
MCP clientCarry messages using a supported protocol revision and handle results and errors correctlyA successful transport response is presented as a completed business action
MCP serverValidate requests and enforce access to the capability it exposesA valid-looking contract identifier bypasses record-level access
Contract serviceEnforce the authoritative business transition and return its durable resultAn expired approval or changed contract is accepted because an upstream component already checked it
Operating ownerDefine acceptance, intervention, incident handling, and evidence retentionAn unresolved outcome has no person or team responsible for it

The last two rows are application design, not additional MCP components. They belong in the review because a protocol diagram often stops at the server while the business effect happens farther downstream.

MCP offers three familiar primitives. Tools expose operations with structured inputs, resources provide contextual data, and prompts provide reusable message templates. For this example, get_contract_evidence is a tool; a policy document could be a resource; and a renewal-review template could be a prompt. The host controls how those pieces enter the interaction. The SDK’s first-steps guide shows the distinction and registration patterns.

A read tool is useful when retrieval itself needs parameters and validation, while a resource lets the application attach identified context. Either can deliver stale, incomplete, or hostile content, so choosing the primitive does not settle whether the evidence is trustworthy. A prompt template also remains subject to the destination application’s permissions; it cannot grant access by describing a workflow that requires it.

An MCP server need not be another autonomous agent. A small deterministic service is often easier to reason about. Persistent memory, planning, policy selection, and coordination between agents belong to the surrounding application unless explicitly implemented elsewhere. Keeping these responsibilities visible makes it possible to replace a model without accidentally replacing the business controls with it.

Specify the protocol revision before drawing the sequence

The protocol changed materially after the first version of this article. Revision 2026-07-28 uses per-request version and capability metadata. It does not require the initialization handshake used by 2025-11-25 and earlier. A modern server supports server/discover, which a client can use to learn capabilities and supported versions, but a discovery call is not a universal prerequisite to every operation. Unsupported versions have a defined error response. Dual-era implementations can support both behaviors. See the versioning and compatibility specification.

That distinction should appear in a design document and a test result. “Supports MCP” is too broad to tell an operator why two components fail to connect. Record the host version, SDK package version, requested protocol revision, transport, and any optional features the workflow depends on. Test the exact combination you intend to release. An older, supported deployment may legitimately use the older handshake; calling that sequence current for every deployment would be misleading.

For a modern read request, the useful sequence is:

  1. Configure a trusted server target and establish the appropriate process or network access.
  2. Select a supported protocol revision and send the required metadata with each request. Discover server capabilities when the client needs them.
  3. Obtain the relevant tool definitions, including input schemas, and restrict the host’s available tools to the task.
  4. Validate the proposed call against the task and send tools/call with the tool name and arguments.
  5. Check the protocol result, tool outcome, and returned evidence before using it in the answer.

The first and fourth steps contain application controls as well as protocol behavior. A valid request can still ask for the wrong record. A valid response can still contain evidence that is unsuitable for the decision.

stdio and Streamable HTTP

With stdio, the client launches a server process and exchanges newline-delimited JSON-RPC messages through its standard streams. Diagnostic messages belong on standard error; ordinary application output must not corrupt the protocol stream. This is useful for a local tutorial and for deliberately local tools. The official Python SDK passes an allow-listed environment plus explicit env values, rather than the parent’s entire environment. The child still runs with the launching user’s filesystem and network privileges; stdio is not an operating-system sandbox. Other hosts may launch processes differently. The SDK transport guide describes its environment handling, and the stdio binding defines framing and lifecycle.

With Streamable HTTP, a server is reached through an HTTP endpoint. In the 2026-07-28 revision, each request is a POST and the response can be JSON or an SSE stream scoped to that request. The revision removed the older GET stream endpoint and protocol-level sessions. Local HTTP services also need origin validation and appropriate binding and access controls. The current HTTP binding should govern proxy and client configuration.

SSE appearing in a response does not mean the deployment uses the deprecated standalone HTTP+SSE transport. This distinction is worth checking when a gateway or framework says it “supports SSE.” The request path, response handling, and version behavior must match at both ends.

Modern protocol statelessness does not prohibit a business process from keeping state. The contract service can maintain a renewal request, and an application can retain a conversation. State needed across calls should have an explicit identifier and ownership rules; a reused connection should not become evidence that two requests belong to the same user or approval. The base protocol defines the per-request model.

Build a bounded contract-evidence tool

The useful first exercise is to expose a small Python function, discover it from a client, and inspect the result. A contract lookup gives that sequence a practical boundary: a successful read returns evidence, while permission to act remains separate.

The code targets the official mcp==2.3.0 Python SDK and discovers the server’s protocol before asserting 2026-07-28. It uses MCPServer and Client; the tagged SDK source and its release record identify the intended interface. The four Python examples below were run together in an isolated local environment with that SDK version: all five business-function tests passed, and the stdio client discovered the server and returned structured fixture evidence. Additional wire checks confirmed the eight-field output schema and safe errors for malformed, unknown and denied contract identifiers. This verifies the fictional local stdio example; HTTP authorization, an external AI host and real business-system access need their own tests.

Use Python 3.11 or later for this tutorial and create an isolated environment. The direct dependency pin keeps the examples on one SDK interface. It is not a complete dependency lock; record and lock the resolved transitive packages for a reproducible deployment.

python3 -m venv .venv
source .venv/bin/activate
python -m pip install "mcp==2.3.0"

1. Separate the business function from the protocol wrapper

Save this as contract_service.py. The fixed principal and in-memory record are demonstration fixtures. No user identity is accepted from model-supplied arguments, and no real account or contract service is contacted.

from copy import deepcopy
from datetime import datetime, timezone
import re

DEMO_PRINCIPAL = "demo-reader"
READABLE = {DEMO_PRINCIPAL: frozenset({"C-204"})}
RECORDS = {
    "C-204": {
        "contract_id": "C-204",
        "record_version": "7",
        "source_updated_at": "2026-10-18T13:00:00Z",
        "renewal_due": "2026-11-01",
        "renewal_terms": "One further year; draft under review",
        "approval_scope": "read_only",
        "renewal_authorized": False,
    }
}


def contract_evidence(contract_id: str, principal: str) -> dict:
    if not isinstance(contract_id, str) or not re.fullmatch(
        r"C-[0-9]{3}", contract_id
    ):
        raise ValueError("invalid_contract_id")

    # Check access before looking up or disclosing a record.
    if contract_id not in READABLE.get(principal, frozenset()):
        raise PermissionError("not_available")

    record = RECORDS.get(contract_id)
    if record is None:
        # Same public outcome as inaccessible, to limit enumeration.
        raise PermissionError("not_available")

    evidence = deepcopy(record)
    evidence["retrieved_at"] = datetime.now(timezone.utc).isoformat()
    return evidence

The input shape is deliberately small. The caller chooses a contract identifier, not a SQL query, file path, arbitrary URL, or principal. In a real service, authenticated context would supply the principal, and an access-control service would replace READABLE. Letting the model send principal="administrator" would defeat that separation even if the schema were valid.

The result distinguishes source_updated_at from retrieved_at. Fetching an old record now does not make the underlying information current. record_version identifies the returned fixture; it does not prove that the terms are approved or applicable. The renewal_authorized field is false because this example grants read access only. A production action service must derive authorization from its own authoritative checks, rather than trusting a boolean copied from a tool response.

There is another deliberate limit: C-204 is the only readable record. Returning the same public error for unknown and inaccessible records reduces one obvious enumeration path, but does not constitute a complete side-channel defense. The internal audit event can distinguish those reasons under access controls while the model receives a less revealing outcome.

2. Expose the function through MCP

Save this as contract_server.py beside the service file.

from typing import TypedDict

from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError

from contract_service import DEMO_PRINCIPAL, contract_evidence

mcp = MCPServer("Contract evidence demo")


class ContractEvidence(TypedDict):
    contract_id: str
    record_version: str
    source_updated_at: str
    renewal_due: str
    renewal_terms: str
    approval_scope: str
    renewal_authorized: bool
    retrieved_at: str


@mcp.tool()
def get_contract_evidence(contract_id: str) -> ContractEvidence:
    """Read one permitted contract fixture; never renew a contract."""
    try:
        return contract_evidence(contract_id, DEMO_PRINCIPAL)
    except ValueError:
        raise ToolError("invalid_contract_id") from None
    except PermissionError:
        raise ToolError("not_available") from None


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

The wrapper registers a tool and delegates its business behavior. The SDK derives its input definition from the typed function; the service still validates what the identifier means. The TypedDict return annotation gives SDK 2.3.0 an output schema for these eight fields and enables structured content. A bare dict annotation does not publish that schema in this version; the dictionary would appear only in text content. The SDK’s structured-output guide and tagged implementation explain the supported annotations.

Expected validation and access failures become safe ToolError messages. Unknown and inaccessible records still share not_available, preserving the example’s enumeration limit. Unexpected exceptions remain masked to the caller and are logged as failures by the SDK. This is an explicit error boundary; it is not an audit service or a complete production error policy.

A schema controls shape, not truth. A string that matches C-204 can identify the wrong contract; a well-formed timestamp can describe an obsolete snapshot. The service and the task evaluation must check those meanings. Similarly, describing a tool as read only is useful to its consumer, but the implementation and credentials must actually prevent writes.

3. Discover and call it without a model

Save this as contract_client.py. The client launches the server with the same Python executable as its own environment, avoiding an accidental switch to another interpreter or SDK installation.

import asyncio
from pathlib import Path
import sys

from mcp import Client, StdioServerParameters


async def main() -> None:
    target = StdioServerParameters(
        command=sys.executable,
        args=[str(Path(__file__).with_name("contract_server.py"))],
    )
    async with Client(target) as client:
        if client.protocol_version != "2026-07-28":
            raise RuntimeError("Unexpected protocol revision")
        if client.server_info is None:
            raise RuntimeError("Server discovery did not return identity")
        print("Protocol:", client.protocol_version)
        listing = await client.list_tools()
        names = [tool.name for tool in listing.tools]
        print("Tools:", names)
        if names != ["get_contract_evidence"]:
            raise RuntimeError("Unexpected tool surface")

        result = await client.call_tool(
            "get_contract_evidence", {"contract_id": "C-204"}
        )
        if result.is_error:
            raise RuntimeError("Contract lookup failed")
        if result.structured_content is None:
            raise RuntimeError("Contract lookup returned no structured evidence")
        print("Evidence:", result.structured_content)


if __name__ == "__main__":
    asyncio.run(main())

Run python contract_client.py in the environment containing the two other files. The verified local run returned one discovered tool and the C-204 fixture, including read-only approval scope and renewal_authorized: false. The client uses the SDK’s default discovery and verifies the returned revision and presence of server identity. An explicit mode="2026-07-28" pin skips discovery and adopts that revision locally; printing the pinned value would not verify the server’s support. The client pattern follows the SDK’s transport API and protocol-version controls.

The client deliberately checks is_error before inspecting structured content. SDK errors can be represented as tool results, while protocol failures can raise exceptions. The exact handling is documented in the client API and server error guide. Catching every exception and returning “no contract found” would erase the difference between denied access, a bad request, and a broken service.

There is no model API key and no natural-language planning in this exchange. That makes the first test more informative: if the call fails, investigate the interpreter, package versions, process launch, request, or tool implementation before debugging a prompt.

4. Test the service, then the wire, then the host

The business function can be checked using Python’s standard library. Save this as test_contract_service.py.

import unittest

from contract_service import (
    DEMO_PRINCIPAL, READABLE, contract_evidence
)


class ContractEvidenceTests(unittest.TestCase):
    def test_permitted_read_does_not_authorize_renewal(self):
        result = contract_evidence("C-204", DEMO_PRINCIPAL)
        self.assertEqual(result["contract_id"], "C-204")
        self.assertEqual(result["record_version"], "7")
        self.assertEqual(result["approval_scope"], "read_only")
        self.assertFalse(result["renewal_authorized"])
        self.assertIn("retrieved_at", result)

    def test_record_access_is_separate_from_identifier_shape(self):
        with self.assertRaises(PermissionError):
            contract_evidence("C-204", "untrusted-principal")

    def test_bad_identifier_is_rejected(self):
        with self.assertRaises(ValueError):
            contract_evidence("../C-204", DEMO_PRINCIPAL)

    def test_inaccessible_and_absent_have_same_public_error(self):
        READABLE["demo-missing-reader"] = frozenset({"C-999"})
        self.addCleanup(READABLE.pop, "demo-missing-reader")
        failures = []
        for contract_id, principal in [
            ("C-204", "untrusted-principal"),
            ("C-999", "demo-missing-reader"),
        ]:
            with self.assertRaises(PermissionError) as raised:
                contract_evidence(contract_id, principal)
            failures.append(str(raised.exception))
        self.assertEqual(failures, ["not_available", "not_available"])

    def test_caller_cannot_mutate_stored_fixture(self):
        result = contract_evidence("C-204", DEMO_PRINCIPAL)
        result["renewal_authorized"] = True
        self.assertFalse(
            contract_evidence("C-204", DEMO_PRINCIPAL)
            ["renewal_authorized"]
        )


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

Run python -m unittest -v test_contract_service.py. All five service tests passed during preparation of this draft. They examine the small service contract without exercising MCP framing, remote authentication, concurrency, a real database, or model behavior. Keep those distinctions in the test report rather than calling one successful layer an end-to-end pass.

Next, run the SDK client against the subprocess and assert the advertised schema, returned fields, permitted read, malformed argument, and denied record cases. Check both the tool-error flag and any protocol exception. Finally, connect the intended host and ask it to review C-204. The acceptance condition is a supported explanation of the evidence with no renewal attempt. An injected instruction in the fixture should not cause another tool to disclose data or perform an action.

The VS Code MCP documentation describes one possible host configuration. Use the approved executable and reviewed server file in that configuration, and inspect the capabilities presented to the host. Do not turn an unfamiliar package-install command into a trusted server simply because it appears in a working example.

Extend the example to an action without borrowing authority

A renewal operation should have a separate contract. It needs the precise target and terms, a verifiable grant for that action, conditions that remain true at execution, and a destination result. The read tool should not acquire write credentials merely because the assistant has reached the next sentence in its plan.

For the fictional C-204 workflow, a proposed action record might contain these fields. This is an application design sketch, not an MCP-standard object or a signed authorization token.

{
  "operation_id": "renewal-demo-0001",
  "contract_id": "C-204",
  "expected_record_version": "7",
  "action": "renew_contract",
  "terms_reference": "draft-terms-demo-3",
  "approval_reference": "approval-demo-12",
  "requested_effective_date": "2026-11-01"
}

The execution service resolves approval_reference from trusted storage. It checks who granted it, which contract and terms it covers, its time limits, any revocation, and whether the requester may invoke it. A string naming an approval has no authority by itself. In this example, a read-only approval fails the action-scope check and the destination remains unchanged.

Now suppose an authorized approver grants renewal of version 7 under the specified terms. Before execution, the contract advances to version 8. The service needs an explicit rule for whether that change invalidates the grant. A change to a material term should trigger review; an unrelated metadata correction may be handled differently if the approved rule says so. The system must evaluate the relevant change instead of treating either “newer” or “previously approved” as sufficient on its own. This is the practical connection to Version Drift.

Where the destination supports it, combine a version precondition with the write in one transaction or conditional operation. A separate read followed by an unconditional update leaves a gap in which the record can change. When the downstream system cannot support that condition, the design needs another controlled execution mechanism, a narrower operation, or a human-managed exception. An upstream check cannot create atomicity that the destination lacks.

Distinguish acceptance from completion

A successful HTTP exchange establishes that a response arrived. A successful MCP tool result establishes the tool’s reported outcome. A destination may then say that a renewal request was accepted for processing, still pending, or durably applied. These are different observations, and the user-facing status should preserve them.

Observed stateWhat the assistant may sayRequired follow-up
Read evidence returned“The record shows these renewal terms.”Establish suitability and authority before proposing execution
Action denied“Renewal is held because the approval covers reading only.”Route to an authorized review process; do not retry with invented authority
Destination accepted request“The renewal request was accepted and is pending.”Query the operation’s durable status
Destination confirms applied result“The destination confirms renewal under this operation reference.”Retain the result and relevant evidence
Request timed out after dispatch“The outcome is unresolved.”Reconcile by operation reference before any retry

The unresolved state deserves its own handling. If a request times out after the destination committed, retrying can duplicate the effect. If the destination supports an idempotency key, use a stable key tied to the same intended operation and verify its documented retention and conflict behavior. Do not invent a fresh key on each retry. If there is no safe deduplication or status query, hold the operation for reconciliation rather than guessing that nothing happened.

Cancellation is similarly bounded. It can request that processing stop, but it does not necessarily undo an external effect already accepted or completed. Recovery may require a compensating action with its own authorization. The operator needs to know which outcome is established, which remains uncertain, and who owns the next decision.

Choose an ecosystem layer for a specific job

The ecosystem now includes SDKs, agent frameworks, developer hosts, registries, and managed gateways. Their overlap can make an architecture look more mature than it is. Choose each component for the responsibility it actually takes on, and test the particular versions together.

LayerCurrent primary referenceWhat to evaluate
Protocol implementationOfficial Python SDK, version 2.3.0Supported protocol revisions, transport behavior, validation, errors, update policy
Agent-framework adapterLangChain MCP integrationThe current MCPAdapter interface, tool filtering, identity propagation, host execution policy; the documented langchain[mcp]>=1.4.0 namespace is beta
Developer hostVS Code MCP configurationApproved launch targets, trust prompts, capability exposure, local process permissions
Public discoveryOfficial MCP RegistryMetadata and provenance useful for discovery; separate organizational approval and runtime authorization
Managed integrationAmazon Bedrock AgentCore GatewayTarget types, inbound identity, outbound credentials, operation selection, logs, limits, and ownership

This is a selection of roles, not a market ranking or a claim that every host supports every protocol feature. The LangChain interface is a useful example of why copied snippets age: its current documentation uses MCPAdapter and provides migration guidance from langchain-mcp-adapters. Adapter examples should not be mixed with that interface or with the official SDK’s Client class without checking the relevant API and version.

The official registry is now available as a metadata service and remains labelled preview in its documentation. It points to packages and endpoints; it does not execute the service for you. Its public scope is also different from a private organizational catalogue. A verified namespace can help establish publisher identity, but an enterprise still needs to assess the package, its permissions, data handling, and suitability for the intended task.

Deployment patterns and the decisions behind them

Direct connection or gateway

A directly connected, tightly scoped service can be a sensible deployment. A gateway becomes useful when several services need common enforcement, routing, quotas, or evidence collection. For C-204, it might validate inbound access, route the approved tool, attach a correlation reference, and apply a request budget before the contract server handles the operation.

That shared position is also a concentration of privilege and failure. Review whether the gateway preserves the initiating identity and distinguishes the caller’s authority from its own downstream credentials. A gateway that converts every request into an administrator call has simplified connectivity while weakening the permission model.

The gateway must understand the selected transport well enough to handle response streams, cancellation, size limits, and timeouts. A proxy configuration that works for ordinary JSON endpoints may buffer an SSE response or cut it off early. Test those behaviors with the actual backend and client. Place business authorization at the authoritative service as well; passing an ingress check should not make a later record change irrelevant.

Catalogue and discovery

Separate locating a server from discovering its current capabilities and approving their use. A catalogue might say where the contract server lives and which release is approved. Runtime discovery describes the tools the server currently exposes. Task policy decides which of those tools this host and caller may use now.

Maintain an internal record with a named owner, package or image digest, approved endpoint, tool surface, supported versions, data classification, credential source, review date, and revocation route. These are recommended operating fields, not a mandatory MCP registry schema. Compare the deployed capability surface with the approved record after each release. An unexpected renew_contract tool appearing beside a read lookup should trigger review even if its schema is valid.

A registry entry does not prove that a server is healthy, and a successful tools/list response does not prove that the backend can execute the operation. Health checks should distinguish process availability, protocol compatibility, and a safe functional check. Avoid a “health” probe that makes a real business change.

Containerized service

Containers can make dependencies and deployment versions easier to control. For an MCP server, the useful operating questions are concrete: which files are mounted, which destinations can the process reach, which credentials can it read, and how much memory or CPU can one request consume? A container with broad mounts and network access can still expose a large part of the environment.

Pin the deployment artifact, run with the minimum required privileges, keep secrets outside the image, and separate tool permissions by capability where practical. A read-only evidence server should not carry renewal credentials for convenience. Review changes to the base image and dependencies with the same care as changes to the tool code.

Scale according to the bottleneck. Adding MCP replicas cannot increase a downstream API’s quota or remove contention on the same contract record. Limit concurrency at the dependency boundary and use load tests that include slow responses and throttling. During a rollout, keep old and new clients compatible with the selected tool contract, and establish how to roll back the service without duplicating partially processed operations.

Managed gateway or hosted service

AgentCore Gateway provides one concrete managed option: its documented MCP target types include OpenAPI specifications, Lambda functions, and MCP servers. It distinguishes inbound access configuration from credentials used to reach targets. Those are useful facilities, but the generated interface still needs an operation-level review. See AWS’s gateway concepts.

An OpenAPI specification can include administrative and destructive endpoints that should never enter an assistant’s tool set. Select business capabilities deliberately, review generated descriptions and schemas, and test identity and failure behavior through the managed route. Include service limits, regional availability, data handling, and exit or migration requirements in the choice. Managed operation transfers some infrastructure work; it does not transfer the organization’s decision about which actions are permitted.

Security controls need an observable refusal path

The most useful security question is what happens when a request should be denied. Can the system demonstrate that the destination was unchanged, or does it only show an assistant saying that it refused? The latter leaves open whether another part of the workflow already made the call.

For remote HTTP services, the MCP authorization specification describes an OAuth-based flow and its requirements. Authorization is optional at the overall protocol level; that is not a recommendation to expose restricted business records without access control. Its HTTP guidance differs from stdio, where credentials normally come through the environment. Follow the applicable flow and protect each resource with appropriately issued credentials. The authorization specification is the primary reference.

Keep inbound and downstream token boundaries explicit. An MCP server using HTTP authorization must validate that a token was issued for its resource and reject tokens intended for another resource. Token passthrough is prohibited; obtain appropriately scoped downstream credentials rather than forwarding a client’s token as downstream authority. The official security guidance explains these requirements and the OAuth-proxy confused-deputy attack, SSRF, local-server risks, and related controls.

A service account can be appropriate for a narrowly delegated machine workflow. The requirement is to establish its permitted purpose and enforce the relevant user or workload authority, not to claim every operation must literally use an end user’s credential. The dangerous design is an agent that acquires broader authority merely because the server happens to possess it.

Risk and verification matrix

The examples below are synthetic test ideas. They are not reports of incidents or a guarantee that passing them exhausts the attack surface.

RiskExample failureControl to implementEvidence to collect
Prompt injection through contentA contract note instructs the assistant to export other recordsTreat returned content as untrusted; restrict available tools and destinations; authorize each action outside the modelInject the note and verify no export occurs in destination and network records
Unprotected or wrongly scoped accessA caller retrieves C-204 with no valid read authorityValidate identity and record access at the service, including direct calls that bypass the hostAllowed and denied principal tests; no record content in denial response
Confused deputy: general privilege patternA low-privilege caller uses the server’s broad credential to renewBind the requested operation to verified user or workload authority and destination rulesRead-only approval cannot cause a write, even through a direct tool request
Token audience or delegation errorA token for one API is accepted by another serviceValidate intended resource, issuer, expiry, and scopes through the selected authorization designWrong-resource and expired credentials fail before business execution
Supply-chain compromiseA server update gains access to files or outbound destinationsReview and pin artifacts; scan dependencies; restrict process access and egress; retain rollback pathArtifact identity plus observed file/network limits in a controlled test
Tool collision or changed descriptionAn unapproved server offers the same renewal tool nameBind tool selection to reviewed server identity and capability version; review changesDuplicate or altered capability is excluded or held for review
Unsafe URL or metadata fetchDiscovery leads a client to an internal service it should not contactValidate destination and redirects; restrict network access at the relevant fetch boundaryControlled disallowed destination is blocked without contacting it
Cross-user cache or state leakOne caller receives another caller’s contract evidencePartition by authorization context and validate ownership of handles on every callAlternate callers and tenants; no cross-boundary result reuse
Approval becomes staleTerms change after renewal approvalRevalidate grant conditions and use destination version preconditions where supportedChanged relevant terms cause a hold; no unconditional write
Duplicate effect after timeoutThe first renewal commits, then a retry creates another requestDurable operation reference, supported deduplication, and reconciliation before retryInject a timeout after acceptance; establish a single intended effect
Resource or cost exhaustionThe host repeatedly invokes an expensive reportCall, time, payload, concurrency, and spend budgets; controlled escalationA loop stops at the defined boundary and leaves an explainable status
Sensitive telemetryLogs contain tokens or full contractsData minimization, redaction, access controls, retention, and separated audit evidenceInspect representative success, denial, and error events for leakage

A content filter may catch some malicious instructions, but it cannot turn arbitrary text into trusted policy. The stronger boundary is that text returned from a tool cannot change the permitted action set. If the task is a renewal review, a hostile paragraph should have no path to add an email recipient, increase an approval limit, or select a different destination.

Human review needs the same specificity. “Allow tool?” is inadequate if the reviewer cannot see the target record, proposed terms, relevant evidence, unresolved conflicts, and effect of approval. Bind the approval to what was shown and establish when a later change requires review again. The Agent Orchestration framework develops this separation between proposal, approval, execution, and evidence.

Local servers need special attention because a familiar-looking launch command can execute third-party code with local privileges. Review the package and launch target, narrow the environment and mounts, and use an actual isolation mechanism where the risk requires one. Calling a tool through stdio does not itself restrict its ability to read a credential file or make an outbound request.

Evaluate the whole task and retain useful operating evidence

Protocol conformance is a necessary test layer, but the service has to answer the business question correctly under ordinary and adverse conditions. For C-204, that means identifying the right record, stating the evidence accurately, recognizing the read-only grant, and leaving the destination unchanged. A test that checks only whether get_contract_evidence was called misses most of that behavior.

Use a small, explicit evaluation set before adding more tools. Include permitted records, inaccessible records, malformed identifiers, missing evidence, conflicting versions, and stale approvals. Give each case an expected service outcome and an expected user-facing result. For a future write tool, add partial failure, revoked access, duplicate request, and timeout-after-acceptance cases. Keep those fixtures separate from production data and label their expected outcomes as test expectations.

LayerQuestion answeredUseful evidence
Business-function testDoes the capability enforce its local contract?Inputs, assertions, returned fields, denial behavior
MCP integration testDo the selected client, server, transport, and revision interoperate?Exact package versions, discovery result, calls, structured errors
Host evaluationDoes the assistant choose the right capability and explain evidence correctly?Task, selected tool and arguments, final answer, acceptance judgment
Destination verificationDid the intended state change occur, or remain unchanged when denied?Durable operation status, before/after version, authoritative confirmation
Recovery exerciseCan operators resolve an ambiguous or partial outcome?Reconciliation steps, owner, elapsed effort, final disposition

Logs that explain a decision without copying every secret

A useful event should connect a task to its tool call and destination outcome. Capture an event time, correlation reference, server release, protocol revision, operation name, duration, result classification, and an appropriate reference to the authority decision. Where needed, retain the evidence and approval versions in a controlled audit store. Avoid logging raw credentials, full arguments, or complete returned records by default.

This illustrative event contains application-defined fields. It is not an OpenTelemetry schema or an MCP result object.

{
  "event": "tool_operation_finished",
  "trace_id": "synthetic-trace-204",
  "protocol_revision": "2026-07-28",
  "server_release": "contract-demo-1",
  "tool": "get_contract_evidence",
  "authority_decision": "permitted_read",
  "business_outcome": "evidence_returned",
  "source_version": "7",
  "destination_write_attempted": false,
  "duration_ms": 185,
  "argument_capture": "disabled"
}

The 185 ms duration is invented for this example. In an implementation, measure it at a defined boundary. Make event names and outcome classes stable enough to compare releases. Keep detailed error evidence where authorized, but give the model and general operational dashboards a bounded error description rather than a stack trace containing connection strings or record content.

Metrics and traces

Track request volume, denial and error rates, latency distributions, result sizes, retry counts, unresolved operations, and complete-task acceptance. Split these by a manageable set of dimensions such as tool and release. Putting contract identifiers or arbitrary prompts into metric labels creates uncontrolled cardinality and can disclose sensitive information.

Trace the path through host, client, gateway if present, server, and backend. This distinguishes slow model selection from a slow database or a buffered response stream. OpenTelemetry has MCP semantic conventions, currently marked Development. Pin the convention and instrumentation versions you adopt; its examples should not be treated as proof that every deployment uses a legacy session or initialization sequence.

Operational charts should make competing explanations visible. A fall in calls may mean better tool selection, a broken discovery path, or a new permission denial. A rise in refusals may be the correct consequence of a security fix. Review those changes beside task acceptance and destination outcomes rather than optimizing a single success counter.

For incident review, retain the facts needed to reconstruct the decision within the organization’s data-handling rules: the requested task, approved capability version, relevant source version, authority decision, dispatch reference, and final destination status. Replaying a full conversation is often both excessive and insufficient if those facts were never recorded clearly.

Account for latency, tokens, and recovery

MCP changes how capabilities are reached; it does not guarantee a speed or cost improvement. A faster interface can still sit in a workflow that calls the wrong tool repeatedly. Measure the complete task before claiming that an optimization helped.

For a sequential workflow, a useful accounting model is:

T_task = T_initial_model
       + sum(T_queue + T_network + T_gateway + T_tool_backend
             + T_model_continuation)
       + T_review + T_recovery

These terms are measurement boundaries, not a protocol formula. Avoid double-counting a backend span already included in a server duration. When branches run in parallel, use the critical path for elapsed time rather than adding every branch duration. Human review and recovery may dominate the practical turnaround even when the tool call itself is fast.

Set latency objectives for the operation. An interactive status lookup and a large analytical report should not share an invented universal threshold. Observe median and tail latency, then trace the slow cases. A gateway hop, database lock, token refresh, throttled dependency, or long model continuation calls for a different response.

Token costs belong to the model calls that consume them

Tool descriptions, schemas, returned evidence, conversation history, and repeated recovery instructions can all enter model context. The amount sent depends on the host and model API, not solely on the protocol. Counting only the final answer ignores the earlier calls that produced it.

For each model call, record the provider’s relevant input, cached-input, output, and other billable categories. Apply the actual rate for that deployment. Then add tool/API charges, allocated hosting and observability costs, and operational review or recovery costs. A simple comparison is:

Cost per accepted task = total in-scope cost / accepted tasks

Define “accepted” before comparing configurations. For a renewal review, it could require the right contract, relevant evidence, a correct authority conclusion, and no unauthorized effect. Count failed and abandoned attempts in the numerator. If there are no accepted tasks, report that directly; do not manufacture a finite unit cost.

Consider an entirely hypothetical batch of 1,000 attempted reviews:

Cost componentConfiguration AConfiguration B
Model calls$60$40
Backend/API calls$20$15
Allocated hosting and telemetry$30$35
Human review and recovery$150$180
Total$260$270
Accepted tasks900950
Cost per accepted task$0.2889$0.2842

Here B costs more in total and spends more on review, while producing a slightly lower cost per accepted task. Model spend alone falls by a third, but the operational improvement is much smaller. These invented figures illustrate the denominator; they are neither a forecast nor a benchmark. Also inspect why 50 or 100 tasks were not accepted. A cheaper average cannot excuse a harmful outcome concealed inside it.

Optimize the evidence path before adding memory

Tool filtering can reduce irrelevant schemas presented to the model, but the selector becomes another component to evaluate. Test whether it retains the necessary tool for difficult or unusual cases. A smaller list that excludes the right operation may increase retries or cause an unsupported answer.

Shape tool output around the task. For a renewal review, return the relevant terms, source identifiers, versions, and unresolved conditions instead of an entire account export. Keep a route to additional evidence when the task requires it. Truncation should be explicit; otherwise the assistant may mistake the visible fragment for a complete record.

Caching can reduce backend work, but only if the result remains valid for the caller and task. Partition by the relevant authorization context and query parameters, include source-version or freshness rules, and define invalidation. A recently cached contract must not survive a material change simply because its time-to-live has not expired. Recheck permission at the appropriate boundary after revocation.

A cached backend result can still be sent to the model on every turn, so backend caching alone does not establish token savings. Model-provider prompt caching has its own accounting and eligibility rules. Measure the two separately. The same caution applies to memory summaries: they may reduce context size while dropping a qualification, source reference, or unresolved approval condition. Keep decision-critical evidence recoverable and test the summary against the actual task.

Finally, give the host a bounded budget for calls, elapsed time, output size, and cost. When the budget is reached, return a useful partial result and the unresolved issue. Silently repeating an unsuccessful tool call converts a recoverable problem into a larger bill and a harder incident.

A five-stage pilot with evidence at each step

A five-day plan can organize a small prototype, provided each day ends with evidence about a bounded capability. Treat the days as illustrative milestones: identity integration, data access, or destination changes can require substantially longer before a service is ready for production.

Day 1: Define one capability and its acceptance boundary

Choose a read operation with a clear owner, such as retrieving permitted contract evidence. Write down its inputs, source system, data fields, authorized callers, and expected answer. Include examples of what the assistant must leave unresolved. For C-204, the useful result is a renewal review that identifies read-only authority without implying execution.

Establish the baseline process and an evaluation set before writing the wrapper. Record how long the current task takes, what errors matter, and how acceptance will be judged. The deliverable is a capability definition and test cases, including denial cases, rather than a broad goal such as “connect AI to procurement.”

Day 2: Implement and test the service locally

Build the deterministic business function and the smallest MCP wrapper around it. Use synthetic data first, verify the input contract and return fields, and separate access denial from operational failure in the internal evidence. Keep the public error response appropriately limited.

Record the interpreter, SDK, protocol revision, and dependency lock used for the test. The deliverable is a service whose allowed and denied behavior can be demonstrated without relying on a model. If the underlying function is unreliable, an agent framework will make diagnosis harder rather than repair it.

Day 3: Connect the intended host

Configure the reviewed server target, inspect discovery, and present only the tools required for the task. Run the same evaluation cases through the host. Compare the selected operation and arguments with the intended request, then assess whether the answer distinguishes evidence, uncertainty, and permission correctly.

Introduce controlled hostile text and conflicting evidence. The deliverable includes traces of correct calls, appropriate holds, and known failures. A single successful demonstration is useful for showing the workflow; it is insufficient evidence for expanding its permissions.

Day 4: Exercise the operating boundary

If remote access is needed, implement the actual identity and authorization design, transport configuration, deployment restrictions, and telemetry. Test revocation, bad credentials, malformed requests, unavailable dependencies, timeouts, and recovery. Inspect logs for sensitive content. Verify the caller cannot bypass a host approval screen by invoking the service directly.

A static API key and a log line can demonstrate a mechanism, but do not establish production readiness. The deliverable is evidence about the controls and unresolved gaps, with named owners. A later write capability also requires destination-side preconditions and reconciliation tests.

Day 5: Review outcomes and make a bounded decision

Show the business owner both accepted tasks and failures. Compare task quality, latency, and total cost with the baseline; explain where manual work moved rather than disappeared. Demonstrate how an operator disables the capability and resolves an uncertain outcome.

The decision may be to continue the read pilot, fix a specific failure, add one separately reviewed action, or stop. Define the next scope and its evidence requirement. This gives the owner a decision they can defend without turning a successful prototype into an unsupported claim of production readiness.

Diagnose common failures at the layer that owns them

SymptomFirst evidence to inspectCorrective direction
A previously working client no longer connectsHost/SDK versions, protocol era, transport, actual errorRestore a supported combination or implement the documented compatibility path; avoid blind fallback
The model selects a similarly named but wrong toolReviewed server identity, exposed tool set, descriptions, task traceNarrow and disambiguate capability exposure; test selection again
Discovery succeeds, but calls fail after a releaseApproved manifest, live schema, arguments, backend releaseReconcile the catalogue and deployment contract; roll back or migrate deliberately
A caller sees a record they should not seeIdentity mapping, service access decision, cache partitionEnforce record access at the authoritative boundary; investigate and contain the disclosure
Median latency is good but some tasks stallQueue, dependency, network, streaming, and model spansFix the measured bottleneck; tune bounded timeouts and concurrency
A timeout is followed by duplicate writesDispatch and operation references, destination status, retry policyReconcile before retry; use destination-supported deduplication and conflict handling
Costs rise while tool errors remain lowCalls per task, repeated context, paid API usage, accepted-task countBound loops and output, improve selection, and measure recovery cost
A tool answer omits an important qualificationReturned payload, truncation marker, summary, original evidenceRestore the necessary evidence and evaluate the host’s interpretation
A “local” server reads unexpected filesLaunch environment, mounts, dependency changes, process accessContain the process and narrow privileges; review artifact and configuration
A legacy system remains outside the workflowAvailable API or transaction interface, identity model, authoritative resultBuild a narrow adapter with explicit failure and recovery semantics

Legacy integration deserves more than a wrapper around arbitrary commands. If an older system exposes only a batch process or operator interface, the adapter must define when a request is accepted, when it takes effect, and how the result can be established. An RPA step may be part of that implementation, but a button click is still not proof of a completed transaction.

Keep the adapter’s business operation narrow enough to test. “Get the status of this order” or “submit this approved renewal request” is easier to review than “execute this query” or “operate this screen.” Document what happens when the underlying system changes, who maintains the adapter, and which uncertain outcomes must return to an operator.

Questions that change implementation choices

MCP and an existing REST API

An MCP server can expose selected capabilities backed by an existing REST API. The business API remains responsible for its own rules and state. The extra interface is useful when compatible hosts can reuse it; it is less compelling when it merely wraps one fixed call for one application with no reuse benefit.

MCP and an agent framework

They address different responsibilities. The framework manages the workflow and model interaction; an MCP client reaches server capabilities. Choose compatible versions and inspect the adapter’s behavior, especially identity propagation, tool errors, and lifecycle management. A framework upgrade should not silently broaden a task’s tool set.

A server that supports only an older revision

Use a client that deliberately supports that revision and test the actual combination. Keep the older lifecycle documented as such. A modern-only client is not automatically compatible with a handshake-era server, and catching a connection failure does not prove that a fallback is safe or supported.

Automatic renewal after a successful read

Read access cannot establish renewal authority. The action needs its own grant, conditions, destination enforcement, and confirmation. A model’s confidence in the recommendation is useful input to review, but cannot substitute for those checks.

A gateway as a mandatory component

A gateway can centralize useful controls, but the design should justify that choice. A small, directly connected service may be adequate. Either way, the destination must enforce the operation’s business rules and the system must preserve the relevant identity and evidence.

A cache as a performance fix

A cache helps only where reuse is valid. Evaluate caller access, source changes, freshness, and the actual bottleneck first. Measure backend savings separately from model token costs, and test invalidation and revocation alongside cache hits.

The decision before adding the next tool

The next capability should have an owner who can describe its permitted effect and recognize when it has failed. For C-204, that owner needs to distinguish a retrieved record, a recommendation, an approved renewal, and a confirmed destination result. The common interface makes those steps easier to connect; the implementation must keep their meanings intact.

I would expand the tool set when the existing capability has credible evidence for that entire path, including denial and recovery. A catalogue of callable operations is easy to demonstrate. A service that can stop at the right boundary, explain why it stopped, and recover without duplicating the action is a more useful foundation for the next workflow.

Related reading

Primary implementation references

Protocol and implementation details were checked against the sources below on October 18, 2026. Package and protocol versions are separate; verify both when adapting the example.


Discover more from Ajith Vallath Prabhakar

Subscribe to get the latest posts sent to your email.

Share or cite this article

LinkedInEmail

Ajith Vallath Prabhakar. “MCP for enterprise AI: a practical guide to integration, authority, and operations.” ajithp.com, 17 August 2025. https://ajithp.com/2025/08/17/model-context-protocol-mcp-the-integration-fabric-for-enterprise-ai-agents/