Skip to main content
Glama
starter-series

MCP Server Starter

MCP Server Starter

TypeScript MCP reference implementation + OIDC npm publish path.

Build a small stdio MCP server with a working tool, resource, prompt, smoke test, and package gate.

CI License: MIT

English | 한국어


Part of Starter Series — Stop explaining CI/CD to your AI every time. Clone and start.

Docker Deploy · Discord Bot · Telegram Bot · Browser Extension · Electron App · npm Package · React Native · VS Code Extension · MCP Server · Python MCP Server · Cloudflare Pages


About This Starter

Currently implemented

  • MCP SDK — @modelcontextprotocol/sdk with stdio transport

  • TypeScript — Strict mode (incl. noUncheckedIndexedAccess + verbatimModuleSyntax), ES2023 target, Zod-validated input & output schemas

  • Safety annotations — readOnly/destructive/idempotent hints on every tool

  • Prompts — Guided workflow templates via registerPrompt (SDK v1.29+)

  • Resources — Data exposure pattern with metadata + handler

  • Response helpers — ok() and err() for consistent tool responses

  • Config — Environment variable parsing pattern

  • CI — gitleaks, npm audit, license compliance, ESLint, build, test

  • CodeQL — Static security analysis (push/PR + weekly)

  • CD — OIDC trusted publishing path for downstream packages

  • Dependabot — Automated dependency + GitHub Actions updates

Planned

Nothing on a public roadmap. The starter is intentionally finished; downstream projects extend it. Breaking-change waves (Node EOL, SDK majors) land via Dependabot.

Design intent

Bootstrap a small reference MCP server with one working tool, resource, and prompt. Stdio is the default because every local MCP client speaks it; HTTP transport is shown inline (see below) rather than bundled, to keep the dependency surface minimal. OIDC publishing is documented for downstream packages after you replace the template package name and npm metadata. Globally-unique tool names are enforced by convention — prefix with your module name, since name collisions across servers are the most common foot-gun.

Non-goals

  • Auth primitives, HMAC signing, rate-limiting, audit logging, human-in-the-loop infra. Those belong in a host, not a server template.

  • Express / HTTP transport in the default scaffold. Template stays small; HTTP is documented for those who need it.

  • Opinionated logging, tracing, or observability stack. Pick what fits downstream.

Redacted

None. Public template.

Related MCP server: MCP Server Template

Quick Start

Via create-starter (recommended):

gh repo create my-mcp-server --template starter-series/mcp-server-starter --clone
cd my-mcp-server && npm install && npm run dev

Before publishing, replace the placeholder package name, description, and bin key. npm publish is blocked while package.json still uses the template defaults.

Or clone directly:

git clone https://github.com/starter-series/mcp-server-starter my-mcp-server
cd my-mcp-server && npm install && npm run dev

Adding Tools

Tool names must be globally unique across all MCP servers a client connects to. Prefix with your module name (e.g., mymodule_action instead of action).

Create src/tools/your-tool.ts:

import { z } from 'zod';
import { ok, err } from '../helpers.js';

export const name = 'your_tool';

export const config = {
  title: 'Your Tool',
  description: 'What your tool does',
  inputSchema: {
    input: z.string().describe('Input parameter'),
  },
  // Declare `outputSchema` to return structured results — the 2026 MCP spec
  // requires servers to populate `structuredContent` (validated against this
  // schema) alongside the text mirror. Omit `outputSchema` for free-form
  // text-only tools.
  outputSchema: {
    result: z.string().describe('Processed value'),
  },
  annotations: {
    readOnlyHint: true,
    destructiveHint: false,
    idempotentHint: true,
    openWorldHint: false,
  },
};

export async function handler({ input }: { input: string }) {
  try {
    const result = `Processed: ${input}`;
    // Pass the structured payload as the second arg to `ok()` — it's set as
    // `structuredContent` alongside the text mirror in `content[]`.
    return ok(result, { result });
  } catch (e) {
    return err(`Failed: ${e instanceof Error ? e.message : String(e)}`);
  }
}

Register in src/index.ts:

import * as yourTool from './tools/your-tool.js';
server.registerTool(yourTool.name, yourTool.config, yourTool.handler);

Adding Prompts

Prompts are reusable, parameterized message templates the client can surface to the user or feed to the model. Use registerPrompt (SDK v1.29+) for full control — title + description + Zod-validated argsSchema. See src/prompts/code-review.ts for a templated example.

Create src/prompts/your-prompt.ts:

import { z } from 'zod';

export const name = 'your-prompt';
export const title = 'Your Prompt';
export const description = 'Guided workflow description';

export const argsSchema = {
  param: z.string().min(1).describe('Required parameter'),
};

export function handler({ param }: { param: string }) {
  return {
    description,
    messages: [{
      role: 'user' as const,
      content: { type: 'text' as const, text: `Prompt text with ${param}` },
    }],
  };
}

Register in src/index.ts:

import * as yourPrompt from './prompts/your-prompt.js';
server.registerPrompt(
  yourPrompt.name,
  { title: yourPrompt.title, description: yourPrompt.description, argsSchema: yourPrompt.argsSchema },
  yourPrompt.handler,
);

Adding Resources

Resources expose data to the client (Tools perform actions). See src/resources/server-info.ts for a working example.

Create src/resources/your-resource.ts:

export const name = 'your-resource';
export const uri = 'info://your/resource';

export const metadata = {
  title: 'Your Resource',
  description: 'What this resource exposes',
  mimeType: 'application/json',
};

export async function handler(resourceUri: URL) {
  return {
    contents: [{ uri: resourceUri.href, mimeType: metadata.mimeType, text: '...' }],
  };
}

Register in src/index.ts:

import * as yourResource from './resources/your-resource.js';
server.registerResource(yourResource.name, yourResource.uri, yourResource.metadata, yourResource.handler);

HTTP Transport

This starter uses stdio (the standard for local MCP servers). If you need HTTP transport — for registries like Smithery/mcp.so or remote deployments — use StreamableHTTPServerTransport with Express:

import express from 'express';
import { randomUUID } from 'node:crypto';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { createServer } from 'my-mcp-server';

const app = express();
app.use(express.json());

const sessions = new Map<string, StreamableHTTPServerTransport>();

app.post('/mcp', async (req, res) => {
  const sessionId = req.headers['mcp-session-id'] as string;
  const existing = sessions.get(sessionId);

  if (existing) {
    await existing.handleRequest(req, res);
    return;
  }

  if (!isInitializeRequest(req.body)) {
    res.status(400).json({ error: 'Bad Request: Not an initialize request' });
    return;
  }

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: () => randomUUID(),
  });

  transport.onclose = () => {
    const id = transport.sessionId;
    if (id) sessions.delete(id);
  };

  const server = createServer();
  await server.connect(transport);
  if (transport.sessionId) sessions.set(transport.sessionId, transport);
  await transport.handleRequest(req, res);
});

app.get('/mcp', async (req, res) => {
  const t = sessions.get(req.headers['mcp-session-id'] as string);
  if (!t) return res.status(400).end();
  await t.handleRequest(req, res);
});

app.delete('/mcp', async (req, res) => {
  const t = sessions.get(req.headers['mcp-session-id'] as string);
  if (!t) return res.status(400).end();
  await t.handleRequest(req, res);
});

app.listen(3000);

Why the complexity? Without isInitializeRequest, every POST creates a new transport → "Already connected" errors. Without GET, clients can't receive server notifications via SSE.

Stateless mode — if your server holds no per-session state (every tool call is independent), construct the transport with sessionIdGenerator: undefined and skip the session map. Stateless is simpler to deploy behind a stateless load balancer and is the recommended default for serverless hosts (Cloudflare Workers, Vercel functions). Keep the stateful pattern above only if you need server-initiated notifications or per-session caches.

See the MCP SDK docs for full details.

Testing Locally

MCP Inspector

npm run build
npm run smoke:mcp
npm run pack:check
npx @modelcontextprotocol/inspector node dist/index.js

npm run smoke:mcp starts the compiled dist/index.js stdio entrypoint through the MCP SDK client, calls the example tool, and checks the registered resource and prompt. npm run pack:check runs npm pack --dry-run --json and verifies main, exports, types, bin, and files all point at packed files.

Claude Desktop

Add to ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "node",
      "args": ["/absolute/path/to/dist/index.js"]
    }
  }
}

After Publishing to npm

{
  "mcpServers": {
    "my-mcp-server": {
      "command": "npx",
      "args": ["-y", "my-mcp-server"]
    }
  }
}

CI/CD

CI (every push/PR)

  1. Secret scanning (gitleaks)

  2. Large file detection (>5 MB)

  3. License compliance (blocks GPL/AGPL)

  4. Security audit (npm audit)

  5. Lint (ESLint + TypeScript)

  6. Build (TypeScript compilation)

  7. Test (Jest)

Security & Maintenance

Workflow

What it does

CodeQL (codeql.yml)

Static analysis for security vulnerabilities (push/PR + weekly)

Maintenance (maintenance.yml)

Weekly CI health check — auto-creates issue on failure

Stale (stale.yml)

Labels inactive issues/PRs after 30 days, auto-closes after 7 more

CD (manual trigger)

  1. CI gate (must pass)

  2. Version guard (prevents duplicate releases)

  3. npm publish with OIDC + provenance

  4. GitHub Release

Setup: See docs/NPM_PUBLISH_SETUP.md — trusted publishers configured on/after 2026-05-20 must select npm publish as an allowed action, or releases fail with an authorization error.

Project Structure

src/
├── index.ts              # Server entry — tool/resource/prompt registration + transport
├── config.ts             # Environment variable config
├── helpers.ts            # ok() / err() response helpers
├── tools/
│   └── greet.ts          # Example tool with annotations (replace with your own)
├── resources/
│   └── server-info.ts    # Example resource exposing server metadata (replace)
└── prompts/
    ├── hello.ts          # Zero-arg prompt example (replace with your own)
    └── code-review.ts    # Templated prompt with Zod-validated args
tests/
├── greet.test.js         # Tool tests
├── helpers.test.js       # Helper tests
├── server-info.test.js   # Resource tests
├── hello.test.js         # Prompt tests
└── code-review.test.js   # Templated prompt tests

Scripts

Command

Description

npm run dev

Run with tsx (no build needed)

npm run build

Compile TypeScript

npm start

Run compiled server

npm run smoke:mcp

Start the compiled stdio server and call it through the MCP SDK client

npm run pack:check

Verify package entrypoints and npm tarball contents

npm run template:check

Fail publishing while template placeholders remain

npm test

Build + run tests (pretest auto-builds)

npm run lint

ESLint

npm run version:patch

Bump patch version

License

MIT

Available Tools

1 tool
greetGreetA
Read-onlyIdempotent

Greet someone by name

ParametersJSON Schema
NameRequiredDescriptionDefault
nameYesName to greet

Output Schema

ParametersJSON Schema
NameRequiredDescription
greetingYesThe rendered greeting
languageYesLanguage code of the greeting

TDQS

A3.7/5.0
Behavior3/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

Annotations already declare readOnlyHint, idempotentHint, and destructiveHint, so the description does not need to repeat safety. The description adds no behavioral context beyond what annotations provide, but does not contradict them. Score 3 as annotations carry the burden.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is a single, efficient sentence with no wasted words. It is perfectly concise for a simple tool.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness5/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given the tool's simplicity, one parameter, and existence of an output schema, the description is complete. It adequately explains the tool's purpose without needing more detail.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

Schema description coverage is 100%, so the schema fully explains the parameter. The description adds 'by name' which is consistent but not additional. Baseline score of 3 is appropriate.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose5/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description 'Greet someone by name' clearly identifies the action (greet) and the target (someone by name), which is specific and unambiguous. With no sibling tools, differentiation is not needed.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

No guidance on when to use this tool. While the tool is simple, there is no mention of context or exclusions. A score of 2 reflects lack of any usage direction.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 1 tool updatev1.0.0
    • First observedgreet

TDQS

A3.7/5.0
Disambiguation5/5

With only one tool, there is no possibility of confusion between tools.

Naming Consistency5/5

A single tool has no inconsistencies; its name is simple and clear.

Tool Count3/5

One tool is appropriate for a starter template, but it feels very thin for any real functionality.

Completeness2/5

The server only offers a greeting tool, lacking any broader capabilities that would fulfill a practical domain.

Maintenance

ActivitySlowing
ResponsivenessUnresponsive

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    Not graded
    maintenance
    A production-ready TypeScript template for building MCP servers with dual transport support (stdio/HTTP), OAuth 2.1 foundations, SQLite caching, observability, and security features including PII sanitization and rate limiting.
    4
    22
    -
  • A
    license
    B
    quality
    D
    maintenance
    A TypeScript starter template for building MCP servers with example tools (echo, math operations, time, flight status) and resources (server info, greetings). Provides a modular architecture for easily extending with custom tools and resources.
    4
    16
    ISC
  • F
    license
    Not graded
    quality
    D
    maintenance
    Provides a starting template for building MCP servers with TypeScript, including examples of tools and resources to accelerate development.
    -

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/starter-series/mcp-server-starter'

If you have feedback or need assistance with the MCP directory API, please join our Discord server