# MCP server: give coding agents tools for your own service: build brief

Assembled by AgentGid's starter kit builder from recipe `mcp-server` v0.3 (https://agentgid.com/build/kits/mcp-server/). Data snapshot: 2026-10-12.
Constraints: commercial use; free parts only; crediting authors is fine.
Recipe status: test-built on 2026-10-12 (2 builds) with other parts. This kit differs from the tested one in: HTTP endpoint (left out here). The parts that differ were not test-built. The fixes from the last build are in this version, which has not been built again as a whole.
Licenses and prices change. Before you ship, open the linked AgentGid pages and confirm.

## What to build

A TypeScript MCP server with tools over your own HTTP API (search and get), tested in memory and with the MCP Inspector, plus the lines that connect it to your coding agent.

## Definition of done

- [ ] `npm run build` compiles with `tsc` and no errors; `npm test` passes.
- [ ] Tests connect a real `Client` to the server in memory and call every tool: valid arguments give the expected result and `structuredContent`; invalid arguments give `isError: true` with a message that says what to fix; an API failure gives `isError: true` with a safe message and no URL, key or stack trace in it.
- [ ] The tools work against a fake API that the tests start on `127.0.0.1` with `node:http`, so no real service or key is needed.
- [ ] `MCP_INSPECTOR_SECRET_STORE=memory npx -y @modelcontextprotocol/inspector@2.10.1 --cli node dist/index.js -e API_BASE_URL=http://127.0.0.1:4010 -e API_KEY=fake-key --method tools/list` lists every tool with its description and schema, and a `tools/call` on the search tool returns results while the fake API runs (`npm run fake-api`).
- [ ] Nothing but protocol messages goes to stdout: a test spawns `node dist/index.js`, sends `initialize`, and the first line it reads back parses as JSON-RPC.
- [ ] Every tool has a description that says when to use it, `.describe()` on every argument, and `readOnlyHint: true` on tools that only read.
- [ ] README.md has the lines that connect the server to Claude Code, Cursor, VS Code and Codex.

Left for the person, because no check can do it:

- Put the real API's base URL and key into `.env` and try the server from your agent on real data: decide then whether the tool descriptions make the agent pick the right tool.

## Before you start

These files are already in this folder. Keep them, and never let a scaffolder overwrite them:

- `AGENTS.md`: this brief.
- `CLAUDE.md`: one line that imports AGENTS.md for Claude Code.
- `.mcp.json`: the MCP servers listed under "Tools you have".
- `CREDITS.md`: the license record. Add to it as you add files.
- `kit.json`: this kit as data (parts, versions, checks). `install.txt`: the install commands below. `first-prompt.txt`: the message the person starts you with.

You may do these without asking:

- Install the packages in the Stack section.
- Read the documentation this brief links to.
- Run `npx @modelcontextprotocol/inspector@2.10.1` without asking. Set `MCP_INSPECTOR_SECRET_STORE=memory` when you run it: otherwise it may write `~/.mcp-inspector/` and try the OS keychain. A run with the flags in the wrong order still writes `~/.mcp-inspector/mcp.json`.
- Work without the real API: build and test against the fake API, and name the live run as not verified.
- Use `git` without asking.

Stop and ask the person for:

- an account, a key or a payment;
- a runtime dependency, asset source or API that this brief does not list;
- a check that still fails after two real fixes (say what you tried).

## Stack

Use these parts and versions. Ask before swapping one.

| Part | Choice | Version | Kind |
|---|---|---|---|
| Runtime | Node.js 22.19 or newer | - | runtime |
| MCP SDK | MCP TypeScript SDK (server) | 2.3.1 | dependency |
| Schemas | Zod | 4.6.7 | dependency |
| Language | TypeScript | 7.0.2 | dev dependency |
| Tests | Vitest | 5.0.4 | dev dependency |
| Also | MCP TypeScript SDK (client) (tests call the tools as a host would) | 2.3.1 | dev dependency |
| Also | @types/node (Node types; TypeScript 6 and newer no longer include them by themselves) | 26.6.5 | dev dependency |

Install exactly these versions (the same lines are in `install.txt`):

```sh
node --version   # must print v22.19 or newer
npm init -y && npm pkg set type=module
npm install --save-exact @modelcontextprotocol/server@2.3.1 zod@4.6.7
npm install -D --save-exact typescript@7.0.2 vitest@5.0.4 @modelcontextprotocol/client@2.3.1 @types/node@26.6.5
```

Checks that apply to this kit:

- The kit uses SDK version 2 (2.3.1). Version 1 (`@modelcontextprotocol/sdk`) still installs and has the most examples online; do not mix the two in one project.

## Rules for this project

- One function builds the server (`makeServer(api)`), and `src/index.ts` only starts it. Tests import `makeServer` and never start stdio.
- Never write to stdout in the server: it is the protocol channel. Log with `console.error`.
- Tool results never contain the API key, full request URLs or stack traces: catch API errors and return a short message that tells the model what to try instead.

## Tools you have

These MCP servers are in the config file listed under "Before you start".

- Context7: Up-to-date library docs for the agent
- AgentGid: Live data for choosing tools, assets and APIs while building

If a server's tools are missing from your session, either the person has not approved them yet (Claude Code holds the servers of a project `.mcp.json` as "Pending approval" until the person accepts them), or the config was added after the session started. Ask the person to approve the servers or restart the agent in this folder; until then, use the links in this brief.

## Plan

Work one step at a time. Do not start a step until the previous one meets its "Done when" lines.

### 1. Scaffold

- Check `node --version` (22.19 or newer), then run the install lines of the Stack section.
- `tsconfig.json`: `"module": "NodeNext"`, `"moduleResolution": "NodeNext"`, `"target": "ES2022"`, `"strict": true`, `"outDir": "dist"`, `"rootDir": "src"`, `"include": ["src"]` and `"types": ["node"]`. TypeScript 6 and newer no longer load `@types/*` by themselves: without that line `tsc` fails with "Cannot find name 'process'". Without `include`, `tsc -p .` also takes `vitest.config.ts` and `test/` and fails with TS6059 ("is not under 'rootDir'").
- Scripts in package.json: `build` (`tsc -p .`), `pretest` (`tsc -p .`, so the tests that spawn `dist/` always run the current code), `test` (`vitest run --passWithNoTests`; without the flag Vitest exits with an error while there are no tests yet), `start` (`node dist/index.js`), `fake-api` (`node dist/fake-api.js`).
- Create a placeholder `src/index.ts` (`export {};`) right away: with an empty `src/`, `tsc` fails with "TS18003: No inputs were found in config file".
- Add `vitest.config.ts` with `test: { include: ['test/**/*.test.ts'] }`: Vitest otherwise also runs the tests of a clean copy made inside the project (the Verify step) and of anything else outside `node_modules`.
- `git init`, with a `.gitignore` that holds `node_modules/`, `dist/`, `.env` and `.check/`, a README.md, and a `.env.example` that lists every variable the server reads, starting with `API_BASE_URL=http://127.0.0.1:4010` (the fake API) and `API_KEY=fake-key` (the key the fake API accepts). Keep it up to date as you add variables.
- Done when:
  - `npm run build` and `npm test` run (no tests yet is fine).
  - The kit files listed under "Before you start" are still there.

### 2. API client and fake API

- `src/api.ts`: a small client for the person's HTTP API with `fetch`, the base URL and key from the environment, a 10-second timeout (`AbortSignal.timeout`) and one error type that carries a status and a safe message.
- Until the person describes their API, model it as a notes service: `GET /items?q=` (search) and `GET /items/:id`. Keep the shape in one place, so swapping in the real endpoints touches only `api.ts` and the tool descriptions.
- `src/fake-api.ts`: the same endpoints on `127.0.0.1` with `node:http` and a few items in memory, plus a switch that makes it answer 500, for the error tests. Tests start it on port 0 (a free port); `npm run fake-api` uses port 4010.
- The fake API checks the key as a real one would: `Authorization: Bearer fake-key` passes, any other key gets 401. So the key handling is tested too, and the line in `.env.example` works as it is.
- When `API_BASE_URL` is not set, stop with a message that names the variable; do not fall back to a guessed address.
- Done when: A test starts the fake API on a free port and the client gets, searches and fails as expected.

### 3. Tools

- Register tools with SDK version 2. `inputSchema` is a `z.object(...)` from `zod/v4`; add `outputSchema` and return exactly its fields as `structuredContent`, next to a short text. The SDK does not drop extra fields, so whatever else the API returns would reach the model unannounced:

  ```ts
  import { McpServer } from '@modelcontextprotocol/server';
  import * as z from 'zod/v4';
  
  export function makeServer(api: Api) {
    const server = new McpServer({ name: 'my-service', version: '0.1.0' });
    server.registerTool('search_items', {
      description: 'Search items by words in their title or text. Use before get_item when you do not know the id.',
      inputSchema: z.object({ query: z.string().min(1).describe('Words to look for'), limit: z.number().int().min(1).max(50).optional().describe('How many items to return, 1 to 50 (default 10)') }),
      outputSchema: z.object({ items: z.array(z.object({ id: z.string(), title: z.string() })) }),
      annotations: { readOnlyHint: true },
    }, async ({ query, limit }) => {
      const items = await api.search(query, limit ?? 10);
      return { content: [{ type: 'text', text: items.map(i => `${i.id}: ${i.title}`).join('\n') || 'No matches.' }], structuredContent: { items: items.map(({ id, title }) => ({ id, title })) } };
    });
    return server;
  }
  ```

- Tools: `search_items` and `get_item`.
- An API failure becomes `{ content: [{ type: 'text', text: <safe message with a hint> }], isError: true }`. Do not let the raw error through: the SDK turns a thrown error's message into the result text the model reads, URL and all.
- When a result is an error, the SDK skips `outputSchema` checks; when it is not, a missing or wrong `structuredContent` becomes an error result ("Output validation error").
- Read first: `https://ts.sdk.modelcontextprotocol.io/v2/servers/tools`; `https://ts.sdk.modelcontextprotocol.io/v2/servers/errors`
- Done when:
  - In-memory tests (`InMemoryTransport.createLinkedPair()` from `@modelcontextprotocol/server`, `Client` from `@modelcontextprotocol/client`) cover every tool: a hit, no hit, invalid arguments, a 500 from the fake API, and a check that no result text contains the key or the API's base URL.
  - Calling a tool that is not registered throws (`ProtocolError: Tool ... not found`) instead of returning `isError`: test that case with `rejects`.

### 4. Stdio entry

- `src/index.ts` checks the configuration once, before serving, then starts the server with `serveStdio` and logs one line to stderr. Calling `apiFromEnv()` only inside the factory hides a missing variable: the server starts and answers `initialize` with "Internal server error", and the message that names the variable is never shown:

  ```ts
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
  import { makeServer } from './server.js';
  import { apiFromEnv } from './api.js';
  
  const api = (() => {
    try { return apiFromEnv(); } catch (e) { console.error((e as Error).message); process.exit(1); }
  })();
  const handle = serveStdio(() => makeServer(api));
  console.error('my-service MCP server on stdio');
  process.on('SIGINT', () => { void handle.close(); });
  ```

- Read `.env` with Node's own `process.loadEnvFile(path)` when the file exists; no extra package is needed. Resolve the path from the code, not the working folder: `fileURLToPath(new URL('../.env', import.meta.url))`. An agent starts the server from its own folder, where a bare `loadEnvFile()` finds no `.env`. Load it at the top of `index.ts`, before `apiFromEnv()`. A variable that is already set (for example by the agent's MCP config) wins, because `loadEnvFile` does not overwrite it; so a test for a missing variable sets it to an empty string, `apiFromEnv()` treats an empty value as missing, and a developer's `.env` cannot fill it in.
- Read first: `https://ts.sdk.modelcontextprotocol.io/v2/serving/stdio`
- Done when:
  - A test spawns `node dist/index.js`, writes an `initialize` request to its stdin and reads one JSON-RPC response from stdout, with nothing before it.
  - The Inspector CLI lists and calls the tools (see Definition of done).

### 5. Connect it to the agent

- Write into README.md how to add the built server: Claude Code `claude mcp add my-service -e API_BASE_URL=... -e API_KEY=... -- node /absolute/path/to/dist/index.js` (the name goes before `-e`, which otherwise reads it as another variable), or a project `.mcp.json` with `command` and `args`; Cursor `.cursor/mcp.json` with `mcpServers`; VS Code `.vscode/mcp.json` with a top-level `servers` object; Codex `[mcp_servers.my-service]` in `~/.codex/config.toml` with `command` and `args`. Leave the kit's own `.mcp.json` as it is (it holds your tools for building); show a project `.mcp.json` for the built server as an example in the README.
- Pass the API key through the config's `env` block, not as a command-line argument.
- Read first: `https://code.claude.com/docs/en/mcp`
- Done when: The README lines use `/absolute/path/to/dist/index.js` as a placeholder, say how to print the real one (`realpath dist/index.js`), and name every variable the server reads.

### 6. Verify

- Commit, make a clean copy of the repository in a temporary folder (`git clone . /tmp/check`, or a git-ignored `.check/` inside the project if you may not write outside it) and follow only the README there: install, build, test, and run the Inspector CLI against the fake API.
- Add to CREDITS.md the packages that ship with the server but are not in the Stack: `npm ls --omit=dev --all` lists them (with SDK 2.3.1 that is `@modelcontextprotocol/core`), with their licenses.
- If the person gave a real API base URL and key, run the Inspector CLI against it once and write the result into PROGRESS.md; otherwise say that the live API was not verified.
- Done when: Every line under Definition of done is true, and anything that could not be verified is named in PROGRESS.md.

## How to check your work

- After each change to a tool, run `npm test` and one Inspector CLI call; read the text the model would see, not only the status.
- Write tool descriptions for the model: say when to use the tool and what to call first. Re-read them after every change.

## Known traps

- SDK version 2 is a different package set: import from `@modelcontextprotocol/server` (and `.../server/stdio`), not from `@modelcontextprotocol/sdk/server/mcp.js`; use `registerTool` with a `z.object` schema instead of `server.tool()`; start stdio with `serveStdio(factory)` instead of `new StdioServerTransport()` plus `connect`. (SDK v2 docs)
- TypeScript 6 and newer no longer include `@types/*` automatically; the SDK's published types reference `Buffer`, so `tsc` fails until `"types": ["node"]` is in tsconfig. (SDK README, and measured with @modelcontextprotocol/server 2.3.1, TypeScript 7.0.2 and Node 22.22, Oct 12, 2026)
- Anything printed to stdout in a stdio server breaks the protocol: the host parses every stdout line as a JSON-RPC message. (SDK v2 docs)
- The SDK turns an exception thrown in a tool handler into the result text the model reads, word for word: an error that contains a URL with a key in its query string hands the key to the model. (SDK v2 docs, and measured with @modelcontextprotocol/server 2.3.1, TypeScript 7.0.2 and Node 22.22, Oct 12, 2026)
- Invalid arguments do not throw on the client: they come back as a normal result with `isError: true` and "Input validation error: ...". Tests that wait for an exception pass when they should fail. (measured with @modelcontextprotocol/server 2.3.1, TypeScript 7.0.2 and Node 22.22, Oct 12, 2026)
- With an `outputSchema`, a result without `structuredContent` (or with the wrong shape) is turned into an error result. (measured with @modelcontextprotocol/server 2.3.1, TypeScript 7.0.2 and Node 22.22, Oct 12, 2026)
- The MCP Inspector 2.10.1 needs Node 22.19 or newer; mode flags (`--cli`, `--web`, `--tui`) must come before the server command, and `-e KEY=VALUE` after it (`--cli node dist/index.js -e API_KEY=fake-key --method tools/list`). Put before the command, `-e` fails with "No servers found in config file". The CLI exits with code 5 when a tool result has `isError: true`, though it prints the result: scripts must not read that as the Inspector failing. (npm registry, `--help`, and measured with Inspector 2.10.1, Oct 12, 2026)
- TypeScript 7 does not export `typescript/bin/tsc`: `require.resolve('typescript/bin/tsc')` fails with ERR_PACKAGE_PATH_NOT_EXPORTED. Run `tsc` through npm scripts (`pretest`), or resolve `typescript/package.json` and join `bin/tsc`. (measured with @modelcontextprotocol/server 2.3.1, TypeScript 7.0.2 and Node 22.22, Oct 12, 2026)
- Vitest 5 writes a small token file, `vitest/.vitest-secret-token`, into the user's data folder on the first `npm test`: `$XDG_DATA_HOME` or `~/.local/share` on Linux, `~/Library/Application Support` on macOS, `%LOCALAPPDATA%` on Windows. If you may not write outside the project, on Linux point `XDG_DATA_HOME` at a git-ignored folder inside it when you run the tests. (Vitest 5.0.4 package source, `getUserDataDir()`)

## Warnings you can ignore

- npm prints audit notices on install. They do not stop the build; do not run `npm audit fix --force`, which can change major versions of the pinned packages.

## Limits

- The server acts with the API key it is given: give it a key with the least rights that the tools need.
- Data the tools return goes to the model provider the agent uses.
- Never read or print the contents of `.env` files, and never commit keys.

## When you need something that is not listed

Do not pick a library, asset source or API from memory. Ask the AgentGid MCP server first:

- `find_tools(goal)` for libraries, APIs and agents that fit a task.
- `find_assets(category, query, free_commercial_only=true)` for media sources and their license terms. It knows sources such as Kenney or Pixabay, not single packs, models or fonts.
- `get_api(api)` for prices, free tiers and limits of entries in AgentGid's API catalog. Media sources are not in it: use `find_assets`.
- `check_status(names)` when a service seems down.

Then tell the person what you want to add and why, cite the AgentGid URL, and wait for a yes before you add a runtime dependency.

## When this brief is wrong

Keep a list of what in this brief was wrong, missing or out of date, with the line and what worked instead (in PROGRESS.md). When you finish, send it to AgentGid's editors with `report_kit_issue(recipe="mcp-server", version="0.3", text=...)` on the AgentGid MCP server: only they read it. Leave out keys, personal data and the person's code.

## Sources

- https://nodejs.org/en/download
- https://agentgid.com/tools/modelcontextprotocol/
- https://agentgid.com/tools/zod/
- https://agentgid.com/tools/typescript/
- https://agentgid.com/tools/vitest/
- https://github.com/modelcontextprotocol/inspector
- https://agentgid.com/mcp/servers/io-github-upstash-context7/
- https://agentgid.com/connect/
- https://ts.sdk.modelcontextprotocol.io/v2/servers/tools
- https://ts.sdk.modelcontextprotocol.io/v2/servers/errors
- https://ts.sdk.modelcontextprotocol.io/v2/serving/stdio
- https://code.claude.com/docs/en/mcp
- https://www.npmjs.com/package/@modelcontextprotocol/server
- https://registry.npmjs.org/@modelcontextprotocol/inspector/latest
- https://registry.npmjs.org/vitest/5.0.4
