DEV Community

Cover image for Implementing a Secure MCP Server
Christoph Hermanns
Christoph Hermanns

Posted on Originally published at cherware.de

Implementing a Secure MCP Server

An MCP server often starts with a seemingly simple requirement: an AI host should find and summarise tickets about a topic. Once current domain data enters a model context, the endpoint is no longer the central question. What matters is which data becomes visible, under whose permissions it is searched, and which actions the server is allowed to expose at all.

This question arose while implementing MCP access for Lutions, a private development project centred on a web application for project and ticket work. There was no security incident behind the decision that followed. Before the first production tool existed, the architectural position was clear: the MCP server must not become a second open product API or an autonomous agent layer. The first article in this series explains the path between host, model, MCP server, and domain system (Hermanns, 2026e). This retrospective shows how that position was put into practice in access design, domain permissions, and operations.

For orientation, the host is the AI application that passes a user question and permitted MCP capabilities to a model. The MCP server then supplies bounded context or performs a permitted domain operation; it does not answer the user’s question itself.

Cut the access surface before adding capabilities

MCP separates resources, prompts, and tools. Tools are functions a model may request through a host; resources and prompts serve different roles in constructing context. The current specification treats these server capabilities separately and identifies tools as a security-relevant, model-controlled interface (Model Context Protocol, 2026a; Model Context Protocol, 2026b).

In Lutions, this led to a simple but consequential choice: the production core starts with six read-only tools. They search visible tickets, load individual or multiple tickets, and return bounded comments, links, or a compact context bundle. The server describes itself accordingly as read-only, and the status view lists the same six tools as read-only.

This boundary reduces several uncertainties at once. A read tool can still touch sensitive information, so it needs the same visibility and project boundaries as the application itself. But it does not change a ticket’s workflow, ownership, priority, or external effect. The first security question therefore remains manageable: may this token see this information—and may this exact information enter the host context?

The narrow tool surface also helps the model. It is not a security control in itself; tool descriptions are not authorisation decisions. But it reduces misuse and makes it possible to align tool lists more closely with the actual permission context (Model Context Protocol, 2026b).

One tool in practice: search, not free-form access

The gain is not only in the number of tools, but in the contract of each one. For example, search_zockets accepts no universal command. It accepts bounded search text, a result limit from 1 to 50, and optional domain filters. This is a simplified, readable version of the server-side schema:

{
  "name": "search_zockets",
  "readOnly": true,
  "input": {
    "query": "string, maximum 200 characters",
    "limit": "integer, 1 to 50, default 10",
    "status": "optional",
    "priority": "optional",
    "type": "optional",
    "projectKey": "optional"
  }
}
Enter fullscreen mode Exit fullscreen mode

The schema does not replace authorisation. It does, however, prevent search from becoming a hidden “perform arbitrary access” function. Server-side validation and the user context still determine which results can be returned at all.

What a request becomes

A user might ask for open risks and decisions on a topic. The host first uses search_zockets to find visible candidates. Only then does it use get_zocket_context_bundle to load a bounded selection of tickets, optionally visible comments, and links. The model’s answer is created only from that limited material.

In Lutions, research follows a clear sequence: search first, retrieve targeted context second, answer last. This does not prevent a model from misunderstanding content. It does prevent a request from beginning with unbounded access to all tickets or a blanket full-text export. That is the first practical benefit of a read core: current ticket information is available for analysis without abandoning the research boundary or domain permissions.

Permissions remain in the domain system

An MCP connection does not create its own trust zone. It must not reveal tickets or information to an AI host that the associated user could not see in Lutions directly.

The Lutions endpoint therefore uses the existing API-token authentication. For read access, issues:read is the domain basis. mcp:connect additionally marks MCP access and can be enforced after the documented migration path. Project-bound tokens remain limited to their permitted projects over MCP as well, and the endpoint has its own rate limit.

Selection of a dedicated Lutions API token with MCP and read scopes

Figure 1: Test configuration for a read-only MCP connection with mcp:connect and issues:read. Token secrets are not displayed in either the UI or this article.

The MCP server does not invent its own permission model. It translates a request into a domain operation and must respect the same boundaries that apply outside MCP. This follows a core MCP security principle: data access and actions require effective access controls, and the protocol does not enforce that architecture by itself (Model Context Protocol, 2026a).

In retrospect, this is one of the most durable decisions. MCP is an additional integration surface, not a bypass around the UI, API, and policies. Without that boundary, scopes easily become labels and tool descriptions become false security promises.

Audit should explain, not duplicate

Tool calls quickly create a temptation to log everything. For a ticket system, that would be the wrong reflex. Complete arguments, ticket descriptions, comment text, and result payloads would turn audit data into a second, difficult-to-control copy of domain content.

Lutions therefore records a tool call as mcp.tool_called. The audit holds technical metadata: tool name, method, status, duration, actor and token context, project context, result count, and, where applicable, an error code. Full arguments and domain payloads are explicitly excluded. The MCP status view uses those same metadata for operational views and aggregations.

Data-minimising audit view for an MCP tool call

Figure 2: Audit view of an MCP tool call with method, tool name, status, and duration. Identifiers and IP information are masked for publication.

This form of observability does not answer every security question. It does show whether and how a tool was used without replicating the content of a ticket analysis. In my assessment, it is a useful balance: enough evidence for troubleshooting, rate-limit signals, and abuse detection; too little content for the audit trail to become a shadow case file.

The point generalises beyond Lutions. With MCP, it is not enough to decide what a server may do. It also matters which traces a tool call leaves and who can later read them. Security-relevant observability starts with a data-minimisation decision.

Why Lutions currently remains read-only

The active Lutions MCP contract is read-only. Write tools are not part of the production tool inventory. Earlier considerations and pilot contracts for write actions deliberately remain concept and follow-up work, not an implicit approval in ongoing operations.

A read tool supplies context; a write tool creates an effect. A comment, link, or status change is therefore not simply the next type of tool. The concrete approval boundary remains the responsibility of the domain system and host; MCP does not prescribe a particular interaction model (Model Context Protocol, 2026b).

Effect Example MCP access status
Read-only Search, read ticket, read comments Active, with read scope
Write domain actions Internal comment, link, status change No active MCP tool; conceivable only with a separate, server-side secured contract
High-risk actions Administrative, visibility-changing, or destructive actions Blocked for the MCP/agent process, or subject to a separate high-risk process

For possible future write cases, the implementation design uses a server-side sequence of preview, human approval, and execute: short validity, unchanged payload, idempotency, and audit correlation. It is an architectural boundary for later decisions, not a hidden approval for current operations.

What the current state can—and cannot—show

The existing core makes current, visible ticket information available through a bounded tool chain. It can provide search results, ticket details, limited comments, and links for analysis; the status view and audit make technical tool calls traceable.

It does not prove that an AI host decomposes every request correctly, selects the best tool sequence, or reliably evaluates ticket data. Nor does a functioning read integration imply that write agents are ready for production. The general runtime boundaries for preview, approval, and execute are still absent for more consequential action classes.

This is not a flaw alongside the architecture. It defines the next evaluation step: which hosts actually work well with bounded results? What amount of data helps a ticket analysis without overloading context? And when does an additional capability raise value more than it raises cost, attack surface, and explanatory burden?

A concrete model observation

The screenshots document a deliberately small but complete test case. The question names a known ticket reference: the model should summarise its content briefly. The ticket is written in German; the response should be summarized in English. This tests neither open-ended research nor a complex agent task. It tests whether a local host can retrieve a concrete domain fact through MCP in a controlled way and express it clearly.

Test profile. Host: LM Studio, which can integrate MCP servers as a host (LM Studio, 2026). Model identifier: google/gemma-4-e4b from the Gemma 4 family (Gemma Team, 2026). Task: Summarise a German-language ticket in English. Permitted capabilities: only the six read-only tools of the Lutions MCP server. Expected observation: for a known reference, the model selects the single-item retrieval tool and keeps its answer grounded in the ticket. The reference, prompt, and model identifier are recorded; this is not yet a publicly reproducible run with host version, timestamp, and standardised question set.

A local Gemma 4 model selects the read-only get_zocket tool for a known ticket reference

Figure 3: Gemma 4 receives the six read-only tools of the Lutions MCP server and selects get_zocket with the issueRef parameter for a known ticket reference.

The first screenshot shows the visible flow of the local google/gemma-4-e4b model in LM Studio. The host exposes the six read-only Lutions MCP tools. For the known reference, the model chooses get_zocket, not search, batch retrieval, comments, links, or the context bundle. The visible call contains only the required ticket reference as issueRef. The returned ticket content follows in the MCP context and answer view. The technical observation is narrow but real: the model recognises the fitting, bounded domain operation and invokes it.

English summary of a Lutions ticket written in German, retrieved via ‘get_zocket’ from a local Gemma-4 model

Figure 4: The model response condenses the retrieved ticket and captures the essential statements of the test ticket.

The second screenshot shows the resulting answer. It correctly identifies the test ticket as transferring an LLM-wiki idea to Lutions. It captures its core point—a maintained, verifiable knowledge layer rather than an isolated chatbot—and names its three central aspects: knowledge layer rather than chatbot, source code as primary truth, and a process of knowledge-impact check, ingest, test, and knowledge status. The comparison with the ticket confirms that interpretation. For this case, the chain “question → appropriate tool → ticket context → factual answer” is complete.

The observation remains deliberately narrow. It shows that the local model path can retrieve a known reference correctly through a read-only tool and produce a useful English answer, even though the ticket is in German. It says nothing about how reliably Gemma 4 finds unknown tickets, researches across multiple steps, compares sources across results, or handles ambiguous questions. That is where a further evaluation of search planning, context selection, and answer quality begins.

Conclusion

The current Lutions state shows that local models can use MCP access meaningfully when the server does not become a second, uncontrolled entry point into the domain system. This retrospective has shown how that framework emerges from small tools, domain permissions, and data-minimising audit.

The LM Studio case demonstrates single-item retrieval: Gemma 4 selects get_zocket, receives bounded ticket context, and produces a suitable answer. Separately, internal retrieval tests show the multi-ticket path: known references can be loaded through get_zocket_context_bundle and summarised with source binding. Neither is a quality judgement about every model or research task. Together, they are practical evidence that the controlled read path is useful outside a single cloud host.

That is the appeal of this kind of development environment. An MCP integration can be designed and then tested in practice with real domain permissions, bounded context, and observable audit events. The connection can first be observed at a coarse level—and then refined, retested, and developed with intent.

The remaining question is no longer whether the basic connection works, but how well it works in use: how reliably do different models support research and analysis? Which context selections and prompt forms hold up, and where do their limits remain? Once sufficiently robust observations exist, that practical perspective can support an article of its own.

Sources and scope

This is an English adaptation of the German original, MCP-Server sicher entwerfen: Was sich bei Lutions bewährt hat, published on my blog. It is a project-specific technical retrospective, not a general production-security certification. I prepared the English adaptation and the accompanying graphics with the assistance of AI tools and reviewed both before publication.

Primary external sources

Related article

Top comments (0)