The Model Context Protocol makes it easy to expose internal tools to an AI model. Easy enough that my first MCP server went from idea to working prototype in an afternoon. Getting it to a state where a security team would approve it took considerably longer.
This post covers what I'd do differently if I started again, with small code examples using the Python SDK.
A quick refresher
An MCP server exposes three kinds of things to a client such as Claude Desktop, an IDE, or your own agent:
- Tools: functions the model can call, with a JSON schema for arguments.
- Resources: read-only data the client can load into context.
- Prompts: reusable prompt templates.
The client discovers what's available, and the model decides when to use it. The protocol handles the plumbing, which is exactly why it's tempting to expose everything.
Lesson 1: Fewer, narrower tools
My first server exposed a generic run_sql tool. It was flexible and impressive in demos. It was also impossible to secure, impossible to audit meaningfully, and the model regularly wrote queries that scanned entire tables.
The second version had specific tools instead:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("internal-ops")
@mcp.tool()
def get_open_tickets(team: str, limit: int = 20) -> list[dict]:
"""Return open support tickets for one team, newest first. Max 50."""
limit = min(limit, 50)
return tickets.find_open(team=team, limit=limit)
@mcp.tool()
def get_customer_summary(customer_id: str) -> dict:
"""Return plan, status, and last 5 invoices for one customer."""
return crm.summary(customer_id)
Narrow tools are easier for the model to choose between, easier to permission, and easier to explain to an auditor. If a team needs flexible querying, that belongs in a reporting tool for humans, not in an agent's toolset.
Lesson 2: Identity has to flow through
The easiest setup runs the MCP server with one service account. Every request, from every user, has the same permissions. In a company, that's a non-starter.
What I do now is pass the end user's identity with each request and check it inside the tool:
@mcp.tool()
def get_customer_summary(customer_id: str, ctx: Context) -> dict:
user = resolve_user(ctx) # from the client's auth token
if not policy.can_view_customer(user, customer_id):
return {"error": "forbidden", "detail": "You don't have access to this customer."}
audit.log(user=user.id, tool="get_customer_summary", customer_id=customer_id)
return crm.summary(customer_id)
How you resolve the user depends on your transport and auth setup. The principle is the same either way: the tool enforces the user's rights, and the model never sees data the user couldn't see directly.
Lesson 3: Descriptions are part of the interface
The model reads your docstrings to decide which tool to use. Vague descriptions produce wrong tool choices, and those don't throw errors.
Things that helped:
- Say what the tool returns and its limits ("Max 50", "last 5 invoices").
- Say when not to use it ("Do not use for billing disputes; use get_invoice_detail").
- Keep argument names self-explanatory.
teambeatst, andcustomer_idbeatsid.
I review tool descriptions in pull requests with the same care as API changes, because to the model, that's what they are.
Lesson 4: Return errors the model can act on
When something fails, return a structured result instead of raising an exception that becomes a generic error message.
return {"error": "not_found", "detail": "No customer with id CUST-123. Ask the user to confirm the ID."}
Good models use that detail to recover, usually by asking the user a clarifying question. A stack trace gives them nothing to work with.
Lesson 5: Treat resources as a data leak surface
Resources feel harmless because they're read-only. But a resource that returns "all open tickets" can put hundreds of customer records into the model's context in one go. Apply the same permission checks and size limits to resources as to tools, and prefer tools with filters when the data is sensitive.
Lesson 6: Log at the server, not only at the client
Client-side logs tell you what the model asked for. Server-side logs tell you what actually happened in your systems. For audits and incident reviews, the server log is the one that matters. I log user, tool, arguments (redacted), result size, status, and duration for every call.
Lesson 7: Version your server like an API
Once more than one client depends on your MCP server, changing a tool becomes a breaking change. Renaming an argument or tightening a return format can quietly degrade every agent that uses it, because models adapt to the old description in prompts and examples.
What helped:
- Add new tools instead of changing existing ones in incompatible ways, then deprecate the old tool after clients move.
- Keep a changelog of tool additions, description edits, and schema changes.
- Run a small behavioural test suite against the server on every release: a few real requests per tool, checked for both correct calls and correct refusals.
def test_summary_requires_access():
ctx = fake_context(user="analyst-no-crm")
result = get_customer_summary("CUST-123", ctx)
assert result["error"] == "forbidden"
Lesson 8: Decide who is allowed to run servers
On the client side, MCP makes it easy for anyone to connect a new server. That's great for developers and a headache for security. Within a few months, I've seen teams with a long tail of unofficial servers running on laptops, each with its own credentials.
A light process works: an approved list of servers, an owner for each, and a review before any server that touches production data is added. It doesn't need to be heavy. It needs to exist before the list gets long.
What I'd set up on day one next time
- A short list of narrow tools, each with an owner.
- User identity passed through and checked in every tool.
- Docstrings reviewed like API documentation.
- Structured, actionable error results.
- Size limits on every tool and resource.
- Server-side audit logging with redaction.
MCP is a great standard for connecting models to systems. It doesn't decide who should be allowed to do what, though. That part is still on us. If you want to see how a platform approaches connectors and permissions at scale, here's how AI Hive handles integrations.
I work on enterprise agent deployments at AI Hive. What's the first tool you exposed over MCP, and did you keep it?

Top comments (0)