During my vacation, I decided to spend some time learning more about the Model Context Protocol, or MCP for short.
I had already been following the topic and had implemented MCP support in a few of my open-source projects. Still, I wanted to understand the protocol better, experiment with a few possibilities, and, most importantly, revisit some of my earlier decisions to see if they still made sense.
Rather than starting another project from scratch, I thought it would be more useful to revisit what I had already built. After all, these are tools that solve real problems, have their own business rules, and were designed independently of any AI integration.
I ended up spending time with projects like ADR Guard, Repo2C4, DotNetRepoInspector, and Complexity Analyzers.
What made this exercise interesting was that each project had different needs. Some only needed to expose information, others performed more complex analysis, and a few involved situations where an agent could participate in generating files.
As I went through them, I realized that implementing the protocol itself wasn't necessarily the hardest part.
The more interesting questions were about what should be exposed to an agent, how much freedom it should have, how to preserve existing business rules, and how to make sure the results remained reliable.
Those questions ended up shaping most of what I learned.
MCP is useful, but it isn't magic
For anyone who hasn't worked with it yet, MCP is an open protocol that standardizes communication between AI applications and external systems.
The idea is to let a compatible client discover and use capabilities provided by a server without needing to understand every implementation detail.
Think about a tool that analyzes a .NET solution.
Normally, you would run a command, get a report, and interpret the results. An agent could attempt the same thing by executing the CLI and reading its output.
And depending on the situation, that might work perfectly well.
But with MCP, we can expose more specific operations, such as list_projects, get_sdk_metadata, or get_project_reference_graph.
Instead of figuring out which command to execute and how to parse its output, the client receives tools with names, descriptions, defined parameters, and structured responses.
The protocol also supports resources, which make information available to clients, and prompts, which provide reusable interaction templates.
You don't need to implement all of these in every server. In my projects, I focused mainly on tools because they were a natural fit for the capabilities I wanted to expose.
There's an important distinction here, though.
MCP is not an AI agent, and it isn't a language model either.
The server exposes capabilities. The client handles communication and may make those capabilities available to a language model.
That separation allows the same tool to work with different compatible clients without being tied to a particular AI provider.
Conceptually, it's fairly straightforward. Things become more interesting when you start deciding which capabilities to expose and how they should behave.
Not everything needs AI
One of the things I found most interesting was how much traditional software functionality can be reused when working with agents.
Take ADR Guard, for example.
The project can validate Architecture Decision Records, analyze relationships between decisions, and identify potential architectural impacts caused by code changes.
Much of that functionality is deterministic.
If an ADR needs to follow a particular structure, contain certain fields, or comply with predefined validation rules, I don't need a language model to check those conditions.
The application already knows how to do that.
What MCP adds is the ability for an agent to access those capabilities during its own analysis.
A tool like validate_adrs can return the problems found in a set of documents. The agent can then explain the diagnostics, provide context, and suggest improvements.
But there's a boundary I don't want to cross: an agent shouldn't turn a failed validation into an approval simply because it interpreted the document differently.
The tool's result needs to represent what was actually verified.
The same reasoning applies to static analysis, automated tests, contract validation, and plenty of other software engineering tasks.
A language model can help interpret the results, connect related information, and explain what they mean. That doesn't mean it needs to replace the logic responsible for producing those results.
In fact, in many cases, I'd rather it didn't.
When I have a verifiable rule, I want it to produce the same result regardless of which model is being used or how the question was phrased.
Why I didn't want to duplicate business logic
Another thing I tried to preserve was the separation between existing functionality and the MCP interface.
This isn't a concern that appeared because of AI. It's something we've dealt with in software development for a long time.
Imagine maintaining two implementations for validating an ADR: one used by the CLI and another used by the MCP server.
Initially, both might work just fine. Then a few fixes and features come along, and differences start appearing.
A validation rule gets updated in one place but not the other. Diagnostics behave slightly differently. A test covers one execution path but misses the other.
It's the kind of maintenance problem I'd rather avoid.
In ADR Guard, both the CLI and the MCP server rely on the same internal domain services.
In DotNetRepoInspector, the inspection engine remains independent of whoever consumes its results.
The same approach appears in Complexity Analyzers, where the MCP server reuses the complexity analysis capabilities already available through the existing tooling.
The MCP server becomes an adapter.
It receives parameters, enforces operational boundaries, invokes the appropriate services, and returns responses through the protocol.
This may sound like a fairly ordinary architectural decision, and in many ways it is. But it makes a difference.
Business rules remain centralized, tests can be reused, and changes to the protocol don't require rewriting the domain.
It also helps avoid unnecessary coupling to AI SDKs.
If I decide to expose the same functionality through HTTP, a CLI, or another integration in the future, the core implementation is still there.
After more than twenty years working with software, I still find this separation valuable. Integration technologies change, but the domain shouldn't have to change every time they do.
An agent doesn't need the entire repository
While working with DotNetRepoInspector and Complexity Analyzers, another question came up: how much information should a tool return?
It's tempting to expose a single operation that produces a complete report containing everything the application knows.
But an agent doesn't always need all that information.
Imagine asking which projects in a solution are still targeting an older version of .NET.
An initial inventory might be enough to identify the relevant projects. After that, the agent can request additional details only for those that need closer inspection.
There's little benefit in returning every detail about every project in the very first call.
In DotNetRepoInspector, this idea appears in the separation between project inventory, project details, reference graphs, and diagnostics.
Complexity Analyzers follows a similar approach. An analysis can return a summary, and the client can later retrieve detailed evidence for a specific method or member.
It also supports reusing analysis snapshots instead of repeating potentially expensive work.
This can reduce the amount of data transferred to the client, the context consumed by the model, and the work performed by the server.
But there's another benefit I appreciate: each operation becomes easier to understand and test.
A tool designed to answer a specific question usually has a clearer contract than one that tries to handle every possible request.
Of course, that doesn't mean splitting everything into dozens of tiny tools. Too much granularity introduces its own complexity.
The goal is to give the agent enough information to do its job without turning every call into a massive data dump.
What happens when the answer isn't clear?
This also made me think about how uncertainty should be represented.
In Complexity Analyzers, some analyses can produce unknown or inconclusive results.
When that happens, I'd rather preserve that information in the response.
An Unknown result may be less satisfying at first, but it's more accurate than presenting an estimate that isn't supported by enough evidence.
This matters even more when a language model is interpreting the results.
An agent can turn incomplete data into a very convincing explanation. If the original contract doesn't communicate its limitations clearly, it becomes harder to distinguish verified facts from assumptions.
That's why I think MCP tools should expose not only their findings but also relevant diagnostics, limitations, and inconclusive states.
Sometimes, knowing what couldn't be determined is just as useful as knowing what could.
Repo2C4: when the agent can do more than read
Repo2C4 turned out to be one of the more interesting projects to revisit.
Unlike tools focused primarily on querying information, Repo2C4 deals with generating architectural representations using LikeC4.
That introduces an important distinction between identifying facts in a repository and understanding its architecture.
Finding project references, dependencies, or implementation patterns provides evidence. But those facts alone aren't always enough to describe architectural components and their relationships correctly.
In Repo2C4's MCP workflow, the server exposes structured evidence collected from the repository.
A client can use that evidence to propose an ArchitectureModel. The server can then validate the proposed model and generate LikeC4 files deterministically.
The architectural interpretation happens outside the MCP server.
I like this approach because the model can be proposed by an AI agent, another mechanism, or even prepared manually.
The server doesn't need to know who came up with the model. It only needs to understand whether the supplied model satisfies its contracts and what it can safely generate from it.
The more challenging part was handling file writes.
Reading repository information and modifying repository contents are very different responsibilities.
For that reason, generation starts in preview mode, with dryRun=true. Persisting files requires an explicit request, including write=true and dryRun=false.
The implementation also includes checks to prevent conflicts with managed files and to reject proposals that no longer match the expected state.
Alongside the MCP server, the project has a separate agent built with Microsoft Agent Framework. It uses the MCP capabilities and includes a human approval step before applying changes.
I find this separation useful because responsibilities remain visible.
The server understands its operations and constraints. The agent coordinates the workflow and interprets the evidence. Human approval is part of the process before changes are applied.
That doesn't eliminate every risk, of course. Approval needs to be tied to a specific proposal, and the repository may change between preview and execution.
But these are exactly the kinds of details that appear when you move beyond a simple demo and try to build something people can actually use.
Security is more than marking a tool as ReadOnly
Security came up repeatedly during this exercise.
One of my main concerns was controlling filesystem access.
For local servers, I try to work with an explicitly authorized repository root. Tool operations must stay within that boundary, even when paths are supplied by the client.
That means handling relative paths, directory traversal attempts, symbolic links, and oversized files.
Execution time, cancellation, and response size also matter.
A tool that works perfectly when analyzing a few dozen files might behave very differently when pointed at a large repository.
Another concern is the content being processed.
A Markdown document, configuration file, or source code comment might contain text that attempts to influence an agent's behavior.
That content needs to remain data, not become instructions.
In ADR Guard, for instance, returned document content is explicitly identified as untrusted. This makes the boundary clearer for the client consuming the information.
But security annotations don't automatically isolate an application.
DotNetRepoInspector is a good example of why this distinction matters.
Although its MCP tools are marked as read-only, inspecting .NET projects may involve MSBuild evaluation, which can load logic defined by the repository itself.
In other words, declaring a tool ReadOnly doesn't guarantee that no potentially unsafe code can run during the operation.
Likewise, using stdio avoids exposing a network endpoint, but it doesn't solve everything related to process permissions or client behavior.
There's also a data privacy consideration.
An MCP server may control which information it reads and returns, but the client may send that information to an external model, depending on its configuration.
Those are separate trust boundaries.
I think it's important to be clear about them rather than assume that using a standardized protocol automatically makes an integration secure.
MCP, agents, and skills: where does each one fit?
Another project I revisited was AI-Ready .NET Workspace.
Its purpose is to provide a .NET development environment prepared for working with AI tools and agents.
One of the decisions I wanted to make explicit was the separation between concepts that often get mixed together.
An agent can take on a role, such as reviewing architecture or assisting with implementation.
A skill can describe a reusable procedure, such as the checks that should be performed before considering a change complete.
A prompt can organize a particular request or workflow.
MCP, meanwhile, provides concrete capabilities that those workflows can use.
In this workspace, the MCP server exposes only two tools: one for retrieving .NET project metadata and another for reading architectural responsibility rules.
There's no need to provide unrestricted filesystem access, a generic terminal, or arbitrary command execution.
I recorded that decision in an ADR because I wanted the responsibilities to remain clear.
What I found interesting is that an MCP server doesn't necessarily need to be sophisticated.
Sometimes, two well-defined operations with small contracts are enough to solve a real problem.
It's easy to get carried away by what agents can potentially do. Having a specific use case helps keep the design focused.
How do I know whether it actually works?
One thing is having an MCP server that compiles and starts correctly. Another is knowing whether a client can use it consistently.
That's why I tried to include tests beyond the business logic itself.
The projects cover areas such as protocol initialization, tool discovery, real tool calls, invalid arguments, cancellation, and operational limits.
Some tests start the MCP server as a separate process and use the official SDK client to communicate with it.
This helps catch integration problems that ordinary unit tests might miss.
A simple example is standard output.
With the stdio transport, standard output is reserved for protocol communication. If the application starts writing log messages there, it can interfere with the MCP client.
That's an easy detail to overlook when you're used to building console applications.
But there's another distinction worth making: testing the protocol isn't the same as testing an agent's behavior.
An automated test can prove that a tool returns the expected project inventory.
It doesn't guarantee that a language model will choose that tool for the right question, interpret its result correctly, or avoid unnecessary calls.
To evaluate that behavior, we need scenarios that are closer to actual usage.
DotNetRepoInspector, for example, includes deterministic evaluations and compatibility records that distinguish what has been tested from what still requires validation.
That's an approach I want to keep improving.
I also think it's important to distinguish between code that exists in a repository and a package that has actually been published and validated for distribution.
A working implementation doesn't automatically mean the release process is complete.
These details may not be as exciting as watching an agent perform a task, but they matter when people start depending on the tool.
What I still want to explore
MCP continues to evolve, along with its SDKs and the clients that support it.
That leaves several areas I'd like to investigate further.
One is interoperability between different clients. Not just checking whether they can start a server, but comparing how they select tools, handle structured responses, and deal with failures.
Another is understanding how newer protocol revisions affect servers that maintain temporary information, such as the snapshots used by Repo2C4.
I'm also interested in looking more closely at context usage.
Detailed tool responses can be valuable, but they can also increase cost and complexity without providing much additional benefit.
Finding the right balance between summary information and detailed evidence seems worth exploring further.
And there's the distribution side of things: versioning, contract compatibility, NuGet publishing, and MCP server registration.
These topics aren't necessarily the most exciting part of working with agents, but they're important if you intend to maintain a tool beyond the initial experiment.
A few thoughts after spending time with MCP
Spending part of my vacation studying MCP turned out to be a good opportunity to revisit concepts I've been working with for years.
Separation of concerns, reusable business logic, explicit contracts, security, and testing are still important.
The protocol provides an interesting way to make existing capabilities available to agents, but it doesn't remove the need to think about those things.
I also came away with a clearer understanding that not every application needs a sophisticated agent, and not every tool needs AI built into it.
In many cases, combining deterministic software that understands a particular problem with language models that can interpret and explain its results seems like a sensible approach.
For now, I'll keep experimenting with these ideas in my projects.
There's still plenty I want to learn, particularly around interoperability, security, and how agents behave in real-world scenarios.
But one conclusion already seems reasonable to me:
Before thinking about everything an agent could do, it's worth being very clear about what we actually want to let it do.
A quick recap
If I had to summarize what I learned, I'd say MCP becomes particularly useful when it exposes capabilities that already have clear responsibilities.
Reusing existing domain logic is usually better than rebuilding it inside the server. Structured responses and focused queries help control context usage. Inconclusive results should remain inconclusive, even when an AI model is interpreting them.
And tools that can modify the environment need boundaries and authorization mechanisms that reflect what they're capable of doing.
None of this is particularly new to people who work with software architecture.
Maybe that's the interesting part. We're learning to use new technologies, but many of the engineering problems we need to solve are still very familiar.
Further reading
For anyone interested in learning more about MCP, these are some useful starting points:
- Model Context Protocol documentation: a good introduction to the protocol, its main concepts, and the responsibilities of clients and servers.
- Official MCP C# SDK: particularly useful for .NET developers who want to explore how the protocol is implemented.
- MCP for Beginners by Microsoft: examples and practical exercises for getting started.
- Model Context Protocol specification: for a deeper understanding of contracts, transports, and security considerations.
I'm also sharing the repositories I used throughout this study:
ADR Guard | Repo2C4 | DotNetRepoInspector | Complexity Analyzers | AI-Ready .NET Workspace
If you're just getting started, I'd suggest choosing a feature that already exists in one of your own projects.
It doesn't have to be anything complicated. A simple read-only operation with clear parameters and a structured response is enough to start understanding how the protocol works.
From there, add some tests, try different clients, and pay attention to the problems that come up.
That kind of hands-on experimentation is what helped me learn the most.
Top comments (0)