cd /news/developer-tools/building-a-type-safe-mcp-server-in-t… · home topics developer-tools article
[ARTICLE · art-95006] src=techstrong.ai ↗ pub= topic=developer-tools verified=true sentiment=· neutral

Building a Type-Safe MCP Server in TypeScript for Enterprise Data Access

A German B2B intelligence platform built a production-grade MCP server in TypeScript, using Zod schemas and workflow-based tools to provide type-safe enterprise data access to LLM clients. The server, which connects to an AWS AppSync GraphQL API with over a million company profiles, was developed over several months, focusing on type safety, permission boundaries, and testing. It defaults to read-only mode, caps batch requests at 50 IDs, and rejects invalid inputs like a limit of 500 or a country name instead of a two-character ISO code.

read5 min views1 publishedAug 13, 2026
Building a Type-Safe MCP Server in TypeScript for Enterprise Data Access
Image: Techstrong (auto-discovered)

TL;DR — Key Takeaways

  • Production MCP servers should be treated as integration services, not simple API wrappers.
  • Workflow-based tools reduce unnecessary model calls and make validation easier.
  • Zod schemas provide a single source of truth for contracts, validation and TypeScript types.
  • Read-only defaults, explicit mutation controls and rate limits help reduce security risk.
  • Unit tests are important, but real back-end integration testing is essential before release.

Two years ago, finding SaaS companies in Germany with 50–200 employees meant opening our portal, setting filters and exporting the results to Excel. The same request can now become an MCP tool call: An LLM client invokes search_companies with a keyword, country code and result limit, while the GraphQL API returns structured data within the conversation. The first version took one afternoon to build but preparing it for production data took several months. Most of that work focused on type safety, permission boundaries and testing rather than the protocol itself.

The Stack

Our MCP server connects an LLM client to a B2B intelligence platform with over a million company profiles. The existing GraphQL API runs on AWS AppSync and already handles access control, so the MCP layer remains a TypeScript process that uses the official @modelcontextprotocol/sdk. Its role is to validate inputs, translate tool calls into GraphQL requests and return results for the model. Local development runs through aws-vault, which keeps long-lived AWS credentials out of configuration files and the .mcp.json file.

Shape Tools Around Workflows

Our first tool inventory mirrored the GraphQL API, with one MCP tool for each back-end operation. The model often chained several low-level calls to complete a task that users viewed as a single action, and each extra call created another opportunity for malformed arguments. We replaced that design with workflow-based tools for company search, profile lookup and collection access. The planned inventory contained nine tools, but it did not include run_graphql_query because a free-form query surface would be difficult to validate and could turn prompt injection into data exfiltration.

One Schema for Contract and Validation

Each tool uses a Zod schema as the source of truth for its parameters. The MCP SDK converts it into the JSON Schema shown to the client, applies it at runtime and provides the handler with typed arguments. This prevents the advertised input contract from drifting away from the server implementation. A typical search tool looks like this:

import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({ name: "company-data", version: "1.0.0" });

server.registerTool(
  "search_companies",
  {
    description: "Search companies by keyword, with an optional country filter.",
    inputSchema: {
      query: z.string().min(1),
      country: z.string().length(2).optional(),
      limit: z.number().int().min(1).max(100).default(10),
    },
  },
  async ({ query, country, limit }) => {
    const result = await gqlClient.searchCompanies({ query, country, limit });
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  }
);

When the model sent a limit of 500, Zod rejected the request locally because the maximum was 100. A country value such as ‘Germany’ also failed before AppSync because the schema required a two-character ISO 3166-1 alpha-2 code. These errors were readable, so the client could correct its arguments instead of retrying back-end requests. We later capped get_companies_batch at 50 deduplicated IDs, and one schema change updated both validation and handler types.

Least Privilege by Default

The server starts in read-only mode, while write access requires an explicit startup flag. Six of the nine planned tools only read data, and each mutation handler checks mutationsAllowed before contacting the back end. A rejected write names the –allow-mutations flag in its error, while startup logs show the active mode and registered tool count. We kept read and write capabilities in separate tools because structural separation is safer than relying on a mode parameter when the caller is probabilistic.

Rate limits are assigned per intent. The ai_search tool allows five requests per minute, which covers the two to four refinements common in a conversation while interrupting a runaway loop. This does not prevent prompt injection, but it limits what an injected instruction can reach and how often it can call the back end. A read-only server with narrow schemas gives an attacker less room than a generic query tool with mutation access.

Test the MCP Layer as a Back-End Service

Debugging through an LLM client makes it difficult to separate a server defect from model behavior. We tested each handler against a mocked GraphQL client, including invalid queries, excessive limits and blocked mutations, while the mocks captured the variables sent downstream. Those tests found a country-code normalization bug before release, and a smaller integration suite then ran against the real AppSync endpoint using aws-vault credentials. MCP Inspector remained a manual release gate because it allowed direct stdio calls without adding model behavior to the test.

The need for integration testing became clear when create_collection passed every mocked test but failed against the real back end with a null pointer exception in a Lambda resolver. We removed it from registration until the resolver was fixed, so the released server exposed eight tools rather than the nine planned. Unit tests could verify wrapper logic, but they could not prove that the back end accepted the generated operation. The incident also exposed a transport rule: stdout is part of JSON-RPC, so a stray console.log can corrupt the stdio message stream.

Production Lessons

A production MCP server is an integration service between a probabilistic caller and production data, not a weekend API wrapper. Teams should begin with read-only workflows, use hard schema limits and require a visible decision before enabling writes. Logs need enough context to reconstruct which tool ran, which user authorized it and what result the back end returned. Ownership should also be explicit because tool review can otherwise fall between platform engineering and the team shipping the AI feature.

── more in #developer-tools 4 stories · sorted by recency
── more on @mcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-a-type-safe…] indexed:0 read:5min 2026-08-13 ·