Tutorials Logic, IN info@tutorialslogic.com

Build an MCP Server: End-to-End with Tools, Resources, Prompts, and Tests

Build an MCP Server

Building an MCP server is mostly about capability design and operational discipline, not just SDK syntax. A good server has a narrow purpose, explicit contracts, strong validation, clear logging, and a predictable deployment shape.

This page walks through an end-to-end implementation using the official TypeScript SDK because it is the most common starting point and tracks the current spec closely.

Mental Model

A first production-worthy server should be small, read-only where possible, and easy to inspect. Your goal is not to expose everything. Your goal is to expose one domain well.

Suggested Project Layout

Keep protocol wiring, backend adapters, and validation logic in separate files so the server stays reviewable as it grows.

Suggested Project Layout
mcp-policy-server/
  package.json
  tsconfig.json
  src/
    index.ts
    policies.ts
    auth.ts
    logging.ts
  test/
    policies.test.ts
    protocol.test.ts
  • Put backend access code behind dedicated modules instead of mixing it into handlers.
  • Add protocol-focused tests early because schema and capability regressions are common.

Step 1: Pick a Server Boundary

Start by choosing one domain, not one technology. "Company policy server" is a useful boundary. "A random set of internal endpoints" is not. The domain tells you what tools, resources, and prompts make sense together.

For a first server, prefer read-only capabilities. They are easier to secure, easier to test with Inspector, and easier to reason about when the model makes mistakes.

  • Choose one clear domain
  • Prefer read-only first
  • Write capability names before writing code
  • Decide what should never be exposed

Step 2: Create the Runtime and Register Capabilities

Use the SDK to create a server instance, then register capabilities deliberately. Add descriptions that help a host and model understand what each capability is for, and design resource URIs before you implement loaders.

Keep your server bootstrap boring. Most production value will come from the handlers and backend modules, not from fancy startup logic.

Step 3: Validate and Guard the Backend

Every tool and resource read should validate inputs and then call backend logic through small adapter functions. That keeps protocol concerns separated from business logic and makes testing easier.

If a backend call needs policy, approval, or identity-aware filtering, encode it there instead of letting the model decide what looks safe.

  • Schema validation stops malformed inputs
  • Business-rule validation stops unsafe valid-looking inputs
  • Adapter functions keep handlers thin and testable

Step 4: Test Like an Integration Surface

Do not stop at unit tests. An MCP server is an integration surface, so you need startup tests, capability listing tests, valid call tests, invalid call tests, and auth-denied tests.

The MCP Inspector should become part of your development loop. If Inspector cannot explain your server, most hosts will struggle too.

  • Start server and ensure initialize succeeds
  • Verify tools/resources/prompts appear as intended
  • Test at least one invalid input for every tool
  • Record expected auth-denied behavior

Build the Server Around Capability Contracts

An MCP server should be designed from its capability contracts outward. Start by writing the tools, resources, and prompts the host should discover, then implement the backend adapters behind them. This prevents the server from becoming a thin wrapper around internal APIs that were never designed for model use.

For each capability, document the user intent, inputs, output shape, authorization requirement, failure modes, and observability fields. If a tool can write data, also document idempotency, approval expectations, and rollback behavior. If a resource can expose sensitive data, document discovery filtering and read authorization separately.

Keep the server startup path clean. For stdio servers, stdout must be reserved for protocol messages; logs should go to stderr or a file. For HTTP servers, initialize authentication, routing, request limits, and structured logging before exposing capability handlers. Many confusing MCP failures are actually process, framing, or environment problems.

Treat schemas as part of the public API. Strong descriptions, bounded enums, required fields, and structured outputs help both the model and the host. Loose schemas shift ambiguity into runtime behavior, which is harder to debug and easier to misuse.

  • Design capability contracts before backend implementation.
  • Separate discovery metadata from execution logic.
  • Use strict schemas and predictable structured outputs.
  • Keep protocol transport clean, especially stdout for stdio.
  • Log request IDs, capability names, validation status, and backend duration.

Testing a Server Like a Protocol Component

Server tests should cover more than happy-path handler functions. Test initialize negotiation, capability listing, valid calls, invalid schemas, unauthorized calls, backend timeouts, oversized results, and host-visible error messages. A server that passes unit tests but fails capability discovery is still broken from the host perspective.

Create fixture requests that mirror the real JSON-RPC messages a host sends. This catches serialization, naming, and response-shape problems early. For tools, test both the model-facing schema and the backend-facing adapter. For resources, test pagination, not-found behavior, and permission filtering. For prompts, test argument validation and generated message structure.

Use the MCP Inspector or an equivalent protocol-level client before trying a full AI host. The Inspector narrows the problem: can the server start, negotiate, list capabilities, execute a minimal call, and return a valid result? Once that works, host integration issues become much easier to diagnose.

Finally, test operational behavior. Simulate cancelled requests, duplicate writes, expired tokens, missing environment variables, and process restarts. Production MCP servers fail in ordinary infrastructure ways, and the tutorial should prepare readers for those realities.

  • Test protocol negotiation, not only business logic.
  • Exercise invalid input and authorization denial paths.
  • Use Inspector before blaming the model or host.
  • Add regression tests for every production incident.
  • Verify error responses are safe, specific, and machine-readable.

Server Review Exercise

Before calling a server finished, review it capability by capability. For each tool, resource, and prompt, write the user intent, allowed caller, input schema, output shape, authorization rule, failure classes, and audit fields. This turns the server from a demo into a maintainable protocol component.

Then run the same server through three clients: a protocol inspector, a real host, and an automated test fixture. Each client catches different problems. The inspector catches protocol shape, the host catches integration and UX issues, and tests catch regressions.

Finally, simulate bad inputs. Send missing fields, extra fields, unauthorized targets, oversized requests, backend timeouts, and malformed resource identifiers. A server that fails safely is much closer to production than one that only handles perfect calls.

  • Review every capability as an API contract.
  • Test with Inspector, host integration, and fixtures.
  • Exercise invalid input, denial, timeout, and oversized output.
  • Keep safe errors distinct and observable.

Minimal Runnable MCP Server with Tool, Resource, and Prompt

This is a compact but executable TypeScript server using the official SDK and stdio transport.

Minimal Runnable MCP Server with Tool, Resource, and Prompt
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';

const server = new McpServer({ name: 'policy-hub', version: '1.0.0' });

server.registerTool(
  'search_policies',
  {
    title: 'Search Policies',
    description: 'Search approved policy documents by keyword.',
    inputSchema: {
      query: z.string().min(3),
    },
  },
  async ({ query }) => {
    const results = [
      {
        uri: 'policy://security/password-rotation',
        title: 'Password Rotation Policy',
        snippet: `Matched on query: ${query}`,
      },
    ];

    return {
      content: [
        { type: 'text', text: `Found ${results.length} matching policy document.` },
      ],
      structuredContent: { results },
    };
  }
);

server.registerResource(
  'password-policy',
  'policy://security/password-rotation',
  {
    title: 'Password Rotation Policy',
    mimeType: 'text/markdown',
  },
  async (uri) => ({
    contents: [
      {
        uri: uri.href,
        text: '# Password Rotation\n\nRotate privileged credentials every 90 days.',
      },
    ],
  })
);

server.registerPrompt(
  'summarize_policy',
  {
    title: 'Summarize Policy',
    description: 'Summarize a policy for a specific audience.',
    argsSchema: {
      audience: z.string(),
      policyText: z.string(),
    },
  },
  ({ audience, policyText }) => ({
    messages: [
      {
        role: 'user',
        content: {
          type: 'text',
          text: `Summarize this policy for ${audience}:\n\n${policyText}`,
        },
      },
    ],
  })
);

const transport = new StdioServerTransport();
await server.connect(transport);
  • This server is intentionally small enough to test end to end.
  • Expand the backend adapters only after the protocol surface is stable.

Package and Run Commands

Package and Run Commands
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
npx tsc --init
npx tsx src/index.ts
  • Use `tsx` during development for fast iteration.
  • Compile to `dist/` for packaged deployments or client configuration.
Key Takeaways
  • Define domain boundary first.
  • Register only capabilities you can validate and support well.
  • Keep backend adapters separate from protocol bootstrap.
  • Test capability discovery and failure behavior before shipping.
Common Mistakes to Avoid
Starting with broad write tools before basic read-only flows are solid.
Mixing protocol code, auth logic, and data access into one giant handler file.
Testing only happy-path calls and ignoring malformed or unauthorized inputs.

Practice Tasks

  • Build a tiny read-only server for one internal knowledge domain.
  • Add one tool, one resource, and one prompt.
  • Test it through startup, valid call, invalid call, and missing-permission scenarios.

Frequently Asked Questions

TypeScript is a strong starting point because the official SDK is mature and well documented, but choose the language that fits your runtime and team.

Only if you actually need remote deployment. A local stdio server is often the better learning and prototyping path.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.