What I learned about structuring an MCP server

What I learned about structuring an MCP server

This post is about my learnings of how to build, use and govern MCP servers right.

The title is dry on purpose and this is probably deeper than my usual blog on this site. But I’ve accumulated enough experience that I think is worthwhile sharing.

With that, it’s important to say this right out of the gate. I’m not a coder. I didn’t sit down and study MCP architecture, read the specification cover to cover, and then design the perfect MCP server.

I learned this the way I learn most things in the blog.

I needed something. I told an AI agent to build it and it worked. Then once I actually used it I found all the ways it didn’t work quite the way I wanted, and started fixing things.

Somewhere along the way I realized that just wrapping an API with MCP is not enough. Designing and even using a good MCP server requires a lot of thinking.

MCP is not an API: an API is a door for software, an MCP server is a door an agent can understand, and that door can be governed

My original problem was pretty simple.

I wanted my agents to manage infrastructure. VCF, Kubernetes, Tanzu, things I work with every day. I have written about building that first MCP server.

These products already have APIs, so my initial instructions to the AI were basically:

“Here’s the API. Build me an MCP server for it.”

And it did.

That’s one of the insane things about building software in 2026. I can have an idea for an integration in the morning and have an agent happily calling it by lunch without personally writing much of the code.

But “it works” is a very low bar.

The more I started using these MCP servers with real agents, the more I realized that an MCP server shouldn’t just be a different way of exposing an API.

It needs to be designed for the thing consuming it, and in this case the thing consuming it isn’t a human developer.

It’s an LLM.

My explanation of MCP

There are probably much better and more technical explanations of MCP out there, but here’s the mental model that works for me.

An API gives software a door into another system.

MCP gives an AI agent a set of “active” APIs, doors if you will, it can understand.

Instead of an agent having to figure out a gazillion API endpoints, CLI commands, authentication methods, parameters and payloads, an MCP server can present it with tools which describe themselves.

An agent can see something like:

list_vms
get_vm
power_on_vm

and understand, reasonably well, what each one is there to do.

So imagine pointing an agent to VCF (I am bringing infra management examples on purpose, that’s my dance) and telling it, “go install a thing”, or “go troubleshoot this problem”. Now VCF presents over 7k APIs and god knows how many CLI commands. The agent needs to go, search the docs, hopefully they are good, and try its best to do what you asked it to do, looping endlessly through trial and error until it gets somewhere or hits a wall. Think of the time and tokens of inefficient wandering around the system. Now bring in a server that can present a small number of tools which tell the agent also how to use those tools, and in turn it’s doing the API call to the endpoint system.

There’s another important aspect to this: that door can be governed. We can control which capabilities an agent can access rather than simply handing it credentials and hoping its prompt keeps it out of trouble.

I’ve written before about why I think that’s important.

But while working on AgentMinder and building more MCP integrations, I ran into a different problem.

Before you can govern tools well, you have to design the tools well.

Naming

When people consume an API, naming is obviously important. This is something I have been teaching as a VMware instructor many years ago as software is an amorphous thing so naming the right way allows people to know where they are.

But when an LLM consumes your tools, naming drives how software actually behaves.

The LLM has a task. It looks at the tools available to it, their names, descriptions and parameters, and decides which one it thinks will help accomplish that task. I learned this when I asked my agent to list the VMs in VCF and it went straight to a tool called vcf_inventory, yet it didn’t inventory the VMs but the APIs, and the answer was in the thousands. Be too generic with tools like:

get
query
execute
request

and now you’re asking the model to do considerably more interpretation.

Worse, imagine connecting multiple MCP servers.

One has:

get_status

Another has:

get_status

And the third has:

get_status

They may all be perfectly reasonable names inside their individual products. But from the agent’s perspective, I’ve just put three buttons in front of it labeled STATUS and asked it to choose.

So one of the first lessons I learned was ridiculously simple:

Tool names need to be descriptive and distinctive.

You’re not naming a function, you’re leaving clues.

Naming follows the tools, not the other way around

My first instinct was essentially to expose the APIs out, so if the underlying platform has an API operation, expose an MCP tool for it.

Makes sense, right?

Except now I’ve just exposed the API complexity.

The agent still needs to understand how the underlying product works. It still needs to understand which sequence of calls accomplishes a task. It still needs to understand all the strange little implementation details enterprise APIs accumulate over the years.

That’s not really an abstraction (I love abstractions).

A better tool purpose might represent what the agent is actually trying to accomplish.

The agent shouldn’t necessarily care that accomplishing a particular operation requires a GET, extracting some existing state, merging a field, constructing another payload and then doing a PUT.

If you want to update the applications, create it as:

update_application

and let the MCP server deal with the ugly APIs underneath.

The more I built these, the more I started thinking about MCP tools as an abstraction layer between what the agent wants to do and how the endpoint system actually does it.

Don’t go too far the other way

Of course, once I realized this, there was another obvious trap.

Making one giant tool! Muhahaha

manage_everything

Problem solved. I am seeing in front of me a hulk tool. Crude and eager to smash.

Except now the tool means nothing.

If one tool can list VMs, delete VMs, modify networks, create users, reconfigure clusters and order pizza, neither the agent nor a security system has a particularly useful boundary to work with. So there seems to be a need for balance (like everything!).

Too low-level and you’ve recreated the API. Too high-level and you’ve created a magic button.

What I’ve ended up preferring are tools that represent meaningful capabilities.

Things an agent can reasonably understand as a distinct action.

list_vms
get_vm
power_vm
create_application
update_application

Those become building blocks the agent can reason about and, as I’ll get to later, they’re also building blocks we can govern.

The MCP server should know the ugly stuff

Another learning I had is that if something can be deterministic, I don’t want the LLM figuring it out every time.

Enterprise APIs have plenty of weirdness with fields that have to survive a version update. All kinds of stuff like:

Identifiers.

Authentication details.

Pagination.

Product-specific conventions.

Responses containing information I absolutely do not want returned to an agent.

I don’t want to write prompts telling the model:

“Remember, when updating this object you need to GET it first, preserve these three fields, don’t accidentally return the client secret, then PUT the merged object back.”

The agent should say what it wants.

The MCP server should know how to make that happen.

The same goes for environment-specific configuration. We don’t need no stinking snowflakes in the rig.

My MCP tool shouldn’t fundamentally become:

get_vms_from_nirans_vcenter_at_this_particular_address

The capability is list_vms.

Which vCenter, credentials, endpoints and environment configuration sit underneath that should be configuration, not part of the conceptual tool.

That makes the tool easier for the agent to understand and the MCP server much more reusable.

Schemas

The parameters you give an agent matter.

If a tool accepts a giant blob of arbitrary JSON, I’m again pushing interpretation onto the model, and if there are known choices, types and required fields, I want the tool contract to express them.

The less ambiguity I give the agent and its model, the less I’m relying on the model to guess. That doesn’t mean making every tool incredibly restrictive. It means putting deterministic structure where it is needed.

Let the LLM reason about the things that require reasoning. Let software enforce the things that don’t.

Agent topology is just as important

There is an uncomfortable conclusion to all of this.

Let’s say I do everything right.

My tools have excellent names.

They’re scoped around meaningful capabilities.

They hide implementation complexity.

They have clean schemas.

They’re reusable.

And then I connect my agent to eight MCP servers and hand it 150 tools. Have I actually designed a good system?

At some point I started realizing that some of what I thought were MCP design problems weren’t MCP problems at all. They were agent design problems.

If an infrastructure agent has access to VCF, Kubernetes and maybe a monitoring system, that might make perfect sense.

Does it also need my identity-management MCP?

GitHub?

Slack?

Probably not.

Better naming can reduce ambiguity, but there’s an even easier way to stop an agent from confusing two unrelated tools: don’t give it the unrelated tool. Which is super important from a security point of view as well. Take that tool away and you shrink what the agent can reach.

This takes us into specialized agents, tool boundaries, orchestration and what I’ve started thinking of as agent topology.

I am still working this out so I’ll save it for another post.

Intents

Once I stopped thinking of MCP as an API wrapper and started thinking in terms of meaningful capabilities, governing agents became much more interesting.

Consider the difference between giving a security layer this:

POST /api/v1/some/resource

and giving it this:

power_vm

The second one actually means something, and now I can start grouping meaningful capabilities.

Maybe an agent has an intent of:

vm-management

That intent could encompass the specific tools required to inspect and manage VMs without automatically giving the agent the ability to change networking, storage or identity.

That’s something I’ve been working on with AgentMinder.

And it’s one of the reasons this seemingly boring exercise of cleaning up my MCP servers ended up affecting how I think about agent authorization.

It’s difficult to express meaningful intent on top of meaningless tools. (Copyright Niran Evenchen, 2026)

I built these backwards

Looking back, I built my MCP servers backwards.

I started with the APIs because that’s what I had.

Then I exposed them to agents.

Then I watched the agents use them.

Only after that did I start asking what the interface should have looked like in the first place.

I’m sure experienced API and MCP developers will recognize many of these lessons immediately. A lot of this is probably just good interface design in a different lens.

Agents expose bad abstractions very quickly.

A human developer can read documentation, understand some historical API weirdness, write glue code and remember that one endpoint that behaves differently on Tuesdays. An agent sees the tools you gave it and tries to accomplish a goal.

These are the rules I ended up with after actually letting agents loose on mine:

Name tools so the agent can distinguish them.

Scope tools around meaningful capabilities, not simply API operations.

Hide deterministic implementation complexity below the model.

Make the tool contract explicit.

Keep the implementation generic enough that the capability isn’t tied to one environment.

And perhaps most importantly:

Don’t assume better tools can compensate for bad agent design.

That last one is what I’m working through now.

Because apparently fixing my MCP servers just led me to the next problem.

More on this as it develops.