Day 7 · Week 1 Capstone · Spark Phase

Build a Complete
MCP Server

Ship a real, production-ready MCP server from a blank directory — combining every Tool, Resource, and Prompt technique you learned this week.

📅 Day 7 of 90 🏆 Capstone Project 🎯 Week 1 Complete
🏗️ What You're Building Today
A fully functional DevDash MCP Server — a developer productivity assistant that exposes GitHub repositories as Resources, provides code analysis as Tools, and ships reusable prompt templates. By the end of this lesson you'll have a complete, runnable server you can connect to Claude Desktop right now.
Week 1 at a Glance
Before coding, let's cement every concept from Days 1–6. Everything you built this week feeds directly into today's capstone.
Day 1
What Is MCP?
Concepts
  • Host / Client / Server model
  • Three primitives
  • Why MCP beats custom APIs
Day 2
JSON-RPC 2.0
Protocol
  • Request / Response / Notification
  • Handshake lifecycle
  • Error codes
Day 3
TypeScript SDK
SDK
  • McpServer & transports
  • Zod schema validation
  • Claude Desktop config
Day 4
Tools in Depth
Tools
  • Design principles
  • Error handling patterns
  • Pagination & caching
Day 5
Resources Deep Dive
Resources
  • URI templates (RFC 6570)
  • MIME types & blobs
  • Subscriptions
Day 6
Prompts & Sampling
Prompts
  • Reusable prompt templates
  • sampling/createMessage
  • Human-in-the-loop
🎯
Capstone goal: Today you use all six days simultaneously. Tools call external APIs, Resources serve live data, Prompts wrap domain knowledge — all in one cohesive server.
DevDash — Project Specification
A developer dashboard server that connects Claude to GitHub data, runs code complexity analysis, and surfaces actionable prompts for code review and PR writing.
🛠️
DevDash MCP Server
Version 1.0.0 · TypeScript · Stdio transport
Transport
stdio (Claude Desktop)
Tools
4 tools
Resources
3 static + 2 templates
Prompts
3 reusable templates
External APIs
GitHub REST v3
Cache TTL
5 minutes (in-memory)
Feature checklist — everything you'll implement:
Phase 1 — Project Scaffold
Start from a blank directory and produce the full project structure in about 5 minutes.
1
Initialise the package
~1 min
Create the directory, initialise npm, and install the two dependencies you always need.
mkdir devdash-mcp && cd devdash-mcp
npm init -y
npm install @modelcontextprotocol/sdk zod
npm install -D typescript tsx @types/node
npx tsc --init --target ES2022 --module NodeNext \
  --moduleResolution NodeNext --strict true \
  --outDir dist --rootDir srcbash
2
Add npm scripts
~30 sec
Edit package.json so you can run the server with a single command.
{
  "scripts": {
    "start": "node dist/index.js",
    "dev":   "tsx watch src/index.ts",
    "build": "tsc"
  },
  "type": "module"
}package.json
3
Final file structure
target layout
You will create every highlighted file during this lesson.
devdash-mcp/ src/ index.ts ← entry point & McpServer bootstrap tools.ts ← all four tool registrations resources.ts ← static resources & URI templates prompts.ts ← three prompt templates github.ts ← GitHub API client + cache complexity.ts ← cyclomatic complexity analyser tsconfig.json package.json .env ← GITHUB_TOKEN (git-ignored)
Phase 2 — GitHub Client & Cache
A thin wrapper around the GitHub REST API with in-memory TTL caching (Day 4 pattern) and exponential-backoff retry.
// src/github.ts
const GITHUB_TOKEN = process.env.GITHUB_TOKEN ?? '';
const BASE = 'https://api.github.com';
const TTL = 5 * 60 * 1000; // 5 minutes

interface CacheEntry<T> { value: T; expiresAt: number }
const cache = new Map<string, CacheEntry<unknown>>();

async function ghFetch(path: string, params?: Record<string, string>): Promise<unknown> {
  const url = new URL(BASE + path);
  if (params) Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, v));
  const key = url.toString();

  const hit = cache.get(key);
  if (hit && Date.now() < hit.expiresAt) return hit.value;

  let delay = 400;
  for (let attempt = 0; attempt < 3; attempt++) {
    const res = await fetch(url.toString(), {
      headers: {
        Authorization: `Bearer ${GITHUB_TOKEN}`,
        Accept: 'application/vnd.github+json',
        'X-GitHub-Api-Version': '2022-11-28',
      },
    });
    if (res.status === 429 || res.status >= 500) {
      await new Promise(r => setTimeout(r, delay));
      delay *= 2;
      continue;
    }
    if (!res.ok) throw new Error(`GitHub ${res.status}: ${await res.text()}`);
    const data = await res.json();
    cache.set(key, { value: data, expiresAt: Date.now() + TTL });
    return data;
  }
  throw new Error('GitHub API unavailable after 3 retries');
}

export async function searchRepos(query: string, perPage = 10, page = 1) {
  return ghFetch('/search/repositories', { q: query, per_page: String(perPage), page: String(page) }) as Promise<{
    total_count: number;
    items: Array<{ full_name: string; description: string | null; stargazers_count: number; language: string | null; html_url: string }>;
  }>;
}

export async function getRepoMeta(owner: string, repo: string) {
  return ghFetch(`/repos/${owner}/${repo}`) as Promise<Record<string, unknown>>;
}

export async function getFileContent(owner: string, repo: string, path: string, ref = 'HEAD') {
  const data = await ghFetch(`/repos/${owner}/${repo}/contents/${path}`, { ref }) as {
    content: string; encoding: string; name: string; size: number;
  };
  if (data.encoding !== 'base64') throw new Error('Unexpected encoding: ' + data.encoding);
  return Buffer.from(data.content, 'base64').toString('utf-8');
}

export async function getRateLimits() {
  return ghFetch('/rate_limit') as Promise<{ resources: Record<string, { limit: number; remaining: number; reset: number }> }>;
}src/github.ts
💡
Environment variable: Create a .env file with GITHUB_TOKEN=ghp_.... Load it by prepending env $(cat .env | xargs) to your run command, or use a package like dotenv. Add .env to .gitignore.
Phase 3 — Complexity Analyser
A pure-function cyclomatic complexity counter. No dependencies — just regex pattern matching on control-flow keywords. Simple but surprisingly useful.
// src/complexity.ts

/**
 * Approximate cyclomatic complexity via keyword counting.
 * CC = 1 + number of decision points (if/else if/for/while/case/catch/&&/||/?)
 *
 * Rule of thumb: CC 1-10 = low, 11-20 = moderate, 21+ = high risk.
 */
export function analyseComplexity(code: string): {
  score: number;
  rating: 'low' | 'moderate' | 'high';
  breakdown: Record<string, number>;
  lines: number;
  suggestions: string[];
} {
  const patterns: Record<string, RegExp> = {
    if:       /\bif\s*\(/g,
    'else if': /\belse\s+if\s*\(/g,
    for:      /\bfor\s*\(/g,
    while:    /\bwhile\s*\(/g,
    case:     /\bcase\s+.+:/g,
    catch:    /\bcatch\s*\(/g,
    and:      /&&/g,
    or:       /\|\|/g,
    ternary:  /\?(?!\.)/g,   // ? but not optional chaining
  };

  const breakdown: Record<string, number> = {};
  let total = 1; // base complexity = 1

  for (const [name, regex] of Object.entries(patterns)) {
    const count = (code.match(regex) ?? []).length;
    if (count > 0) breakdown[name] = count;
    total += count;
  }

  const rating: 'low' | 'moderate' | 'high' =
    total <= 10 ? 'low' : total <= 20 ? 'moderate' : 'high';

  const suggestions: string[] = [];
  if (total > 10) suggestions.push('Consider extracting nested conditions into named predicates.');
  if ((breakdown.for ?? 0) + (breakdown.while ?? 0) > 3)
    suggestions.push('Multiple loops detected — consider array higher-order methods (map/filter/reduce).');
  if ((breakdown.case ?? 0) > 5)
    suggestions.push('Large switch — consider a lookup table or strategy pattern.');
  if ((breakdown.and ?? 0) + (breakdown.or ?? 0) > 4)
    suggestions.push('Complex boolean expressions — extract into well-named helper functions.');

  return { score: total, rating, breakdown, lines: code.split('\n').length, suggestions };
}src/complexity.ts
Phase 4 — Registering Tools
Four tools following Day 4's verb_noun naming, single-responsibility design, and isError: true error handling. Notice how create_pr_draft uses sampling to make the server "think" on your behalf.
// src/tools.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';
import { searchRepos, getFileContent, getRateLimits } from './github.js';
import { analyseComplexity } from './complexity.js';

export function registerTools(server: McpServer): void {

  // ── Tool 1: search_repos ─────────────────────────────────────────────────
  server.tool(
    'search_repos',
    'Search GitHub repositories. Returns up to 10 results with a pagination cursor.',
    {
      query:   z.string().min(1).describe('GitHub search query (same syntax as github.com/search)'),
      cursor:  z.number().int().min(1).default(1).describe('Page number for pagination (starts at 1)'),
    },
    async ({ query, cursor }) => {
      try {
        const result = await searchRepos(query, 10, cursor);
        const items = result.items.map(r =>
          `• ${r.full_name} ⭐${r.stargazers_count} [${r.language ?? 'unknown'}]\n  ${r.description ?? 'No description'}\n  ${r.html_url}`
        );
        const nextCursor = result.items.length === 10 ? cursor + 1 : null;
        return {
          content: [{
            type: 'text' as const,
            text: [
              `Found ${result.total_count.toLocaleString()} repos. Showing page ${cursor}.`,
              ...items,
              nextCursor ? `\nNext page cursor: ${nextCursor}` : '\nNo more pages.',
            ].join('\n'),
          }],
        };
      } catch (err) {
        return { isError: true, content: [{ type: 'text' as const, text: `search_repos failed: ${String(err)}` }] };
      }
    }
  );

  // ── Tool 2: get_file_content ──────────────────────────────────────────────
  server.tool(
    'get_file_content',
    'Fetch the raw text content of a file from a GitHub repository.',
    {
      owner:  z.string().describe('Repository owner (user or org)'),
      repo:   z.string().describe('Repository name'),
      path:   z.string().describe('File path within the repo (e.g., src/index.ts)'),
      ref:    z.string().default('HEAD').describe('Branch, tag, or commit SHA'),
    },
    async ({ owner, repo, path, ref }) => {
      try {
        const content = await getFileContent(owner, repo, path, ref);
        return {
          content: [
            { type: 'text' as const, text: `File: ${owner}/${repo}/${path} @ ${ref}` },
            { type: 'text' as const, text: content },
          ],
        };
      } catch (err) {
        return { isError: true, content: [{ type: 'text' as const, text: `get_file_content failed: ${String(err)}` }] };
      }
    }
  );

  // ── Tool 3: analyze_complexity ────────────────────────────────────────────
  server.tool(
    'analyze_complexity',
    'Compute approximate cyclomatic complexity for a code snippet and receive actionable suggestions.',
    {
      code:     z.string().min(10).describe('Source code to analyse (any language)'),
      language: z.string().optional().describe('Programming language hint for context'),
    },
    async ({ code, language }) => {
      const result = analyseComplexity(code);
      const ratingEmoji = { low: '🟢', moderate: '🟡', high: '🔴' }[result.rating];
      const lines = [
        `${ratingEmoji} Cyclomatic Complexity: ${result.score} (${result.rating})`,
        `Lines analysed: ${result.lines}${language ? ` | Language: ${language}` : ''}`,
        '',
        'Breakdown:',
        ...Object.entries(result.breakdown).map(([k, v]) => `  ${k}: ${v}`),
      ];
      if (result.suggestions.length) {
        lines.push('', 'Suggestions:');
        result.suggestions.forEach(s => lines.push(`  • ${s}`));
      }
      return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
    }
  );

  // ── Tool 4: create_pr_draft ───────────────────────────────────────────────
  server.tool(
    'create_pr_draft',
    'Generate a pull-request title and description by sampling the AI model with a structured prompt.',
    {
      diff_summary: z.string().describe('A plain-English summary of the diff or changes made'),
      base_branch:  z.string().default('main').describe('Target branch for the PR'),
      ticket_id:    z.string().optional().describe('Jira / Linear ticket ID to include in the title'),
    },
    { readOnlyHint: false, destructiveHint: false },
    async ({ diff_summary, base_branch, ticket_id }, { server: srv }) => {
      try {
        const prefix = ticket_id ? `[${ticket_id}] ` : '';
        const response = await srv.createMessage({
          messages: [{
            role: 'user',
            content: {
              type: 'text',
              text: [
                'Write a concise GitHub pull-request title and description for the following change.',
                `Target branch: ${base_branch}`,
                ticket_id ? `Ticket: ${ticket_id}` : '',
                '',
                'Changes summary:',
                diff_summary,
                '',
                'Format your response as:',
                'TITLE: <one-line title, max 72 chars>',
                'BODY:',
                '<markdown body with ## Summary, ## Changes, ## Testing sections>',
              ].filter(Boolean).join('\n'),
            },
          }],
          modelPreferences: { speedPriority: 0.3, intelligencePriority: 0.8 },
          maxTokens: 600,
          includeContext: 'none',
        });

        const text = response.content.type === 'text' ? response.content.text : '';
        const titleMatch = text.match(/^TITLE:\s*(.+)$/m);
        const bodyMatch  = text.match(/BODY:\s*([\s\S]+)$/m);

        return {
          content: [{
            type: 'text' as const,
            text: [
              `PR Title: ${prefix}${titleMatch?.[1]?.trim() ?? 'Update'}`,
              '',
              bodyMatch?.[1]?.trim() ?? text,
            ].join('\n'),
          }],
        };
      } catch (err) {
        return { isError: true, content: [{ type: 'text' as const, text: `create_pr_draft failed: ${String(err)}` }] };
      }
    }
  );
}src/tools.ts
🔑
Sampling requires host opt-in. For create_pr_draft to work the host (Claude Desktop) must support sampling. Claude Desktop does — it shows a confirmation dialog before the model response is returned to your server.
Phase 5 — Registering Resources
Three static resources (health, config, rate limits) plus two URI template resources (repo metadata and file content) — connecting the Day 5 patterns to real GitHub data.
// src/resources.ts
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { getRepoMeta, getFileContent, getRateLimits } from './github.js';

const SERVER_START = new Date().toISOString();

export function registerResources(server: McpServer): void {

  // ── Static 1: Server status ───────────────────────────────────────────────
  server.resource(
    'devdash-status',
    'devdash://status',
    { mimeType: 'application/json' },
    async () => ({
      contents: [{
        uri: 'devdash://status',
        mimeType: 'application/json',
        text: JSON.stringify({
          server: 'DevDash MCP',
          version: '1.0.0',
          status: 'healthy',
          startedAt: SERVER_START,
          uptime: Math.floor((Date.now() - new Date(SERVER_START).getTime()) / 1000) + 's',
        }, null, 2),
      }],
    })
  );

  // ── Static 2: Server config ───────────────────────────────────────────────
  server.resource(
    'devdash-config',
    'devdash://config',
    { mimeType: 'application/json' },
    async () => ({
      contents: [{
        uri: 'devdash://config',
        mimeType: 'application/json',
        text: JSON.stringify({
          cacheTtlMs: 5 * 60 * 1000,
          maxRetries: 3,
          retryBaseDelayMs: 400,
          githubApiBase: 'https://api.github.com',
          defaultPerPage: 10,
          samplingEnabled: true,
        }, null, 2),
      }],
    })
  );

  // ── Static 3: GitHub rate limits ──────────────────────────────────────────
  server.resource(
    'devdash-limits',
    'devdash://limits',
    { mimeType: 'application/json' },
    async () => {
      const data = await getRateLimits();
      return {
        contents: [{
          uri: 'devdash://limits',
          mimeType: 'application/json',
          text: JSON.stringify(data, null, 2),
        }],
      };
    }
  );

  // ── Template 1: Repo metadata ─────────────────────────────────────────────
  server.resource(
    'github-repo',
    new ResourceTemplate('github://repo/{owner}/{repo}', { list: undefined }),
    { mimeType: 'application/json' },
    async (uri, { owner, repo }) => {
      const meta = await getRepoMeta(String(owner), String(repo));
      return {
        contents: [{
          uri: uri.href,
          mimeType: 'application/json',
          text: JSON.stringify(meta, null, 2),
        }],
      };
    }
  );

  // ── Template 2: File content ──────────────────────────────────────────────
  server.resource(
    'github-file',
    new ResourceTemplate('github://file/{owner}/{repo}/{+path}', { list: undefined }),
    { mimeType: 'text/plain' },
    async (uri, { owner, repo, path }) => {
      const content = await getFileContent(String(owner), String(repo), String(path));
      return {
        contents: [{
          uri: uri.href,
          mimeType: 'text/plain',
          text: content,
        }],
      };
    }
  );
}src/resources.ts
📌
{+path} is a level-2 RFC 6570 path expression — it allows slashes in the variable, so github://file/owner/repo/src/utils/helper.ts maps correctly. Plain {path} would only allow a single path segment.
Phase 6 — Registering Prompts
Three prompt templates that package domain knowledge into reusable conversation starters. The code-review prompt embeds a live GitHub resource so Claude reads the real file — no copy-pasting required.
// src/prompts.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { z } from 'zod';

export function registerPrompts(server: McpServer): void {

  // ── Prompt 1: code-review ─────────────────────────────────────────────────
  server.prompt(
    'code-review',
    'In-depth code review for a GitHub file with severity-rated findings.',
    {
      owner:    z.string().describe('Repo owner'),
      repo:     z.string().describe('Repo name'),
      path:     z.string().describe('File path to review (e.g., src/index.ts)'),
      focus:    z.enum(['security', 'performance', 'readability', 'all']).default('all'),
    },
    ({ owner, repo, path, focus }) => ({
      messages: [
        {
          role: 'user' as const,
          content: {
            type: 'resource' as const,
            resource: {
              uri: `github://file/${owner}/${repo}/${path}`,
              mimeType: 'text/plain',
              text: `[Content of ${owner}/${repo}/${path} will be resolved by the MCP client]`,
            },
          },
        },
        {
          role: 'user' as const,
          content: {
            type: 'text' as const,
            text: [
              `Please review the file \`${path}\` from \`${owner}/${repo}\`.`,
              focus !== 'all' ? `Focus specifically on **${focus}** concerns.` : 'Cover security, performance, and readability.',
              '',
              'Structure your review as:',
              '## 🔴 Critical Issues',
              '## 🟡 Warnings',
              '## 🟢 Suggestions',
              '## ✅ What\'s Done Well',
              '',
              'For each issue cite the line range and provide a concrete fix.',
            ].join('\n'),
          },
        },
      ],
    })
  );

  // ── Prompt 2: write-pr-description ───────────────────────────────────────
  server.prompt(
    'write-pr-description',
    'Generate a structured GitHub pull-request description from a diff summary.',
    {
      summary:     z.string().describe('Plain-English description of changes'),
      base_branch: z.string().default('main').describe('Target branch'),
      ticket:      z.string().optional().describe('Linked ticket ID'),
      breaking:    z.enum(['yes', 'no']).default('no').describe('Does this PR include breaking changes?'),
    },
    ({ summary, base_branch, ticket, breaking }) => ({
      messages: [{
        role: 'user' as const,
        content: {
          type: 'text' as const,
          text: [
            'Create a complete GitHub pull-request description in Markdown.',
            `Merging into: \`${base_branch}\``,
            ticket ? `Linked ticket: ${ticket}` : '',
            breaking === 'yes' ? '⚠️ This PR contains BREAKING CHANGES.' : '',
            '',
            'Changes summary:',
            summary,
            '',
            'Include these sections: ## Summary, ## Motivation, ## Changes, ## Screenshots (if applicable), ## Testing, ## Checklist.',
            'End with a checklist using GitHub markdown checkboxes (- [ ] item).',
          ].filter(Boolean).join('\n'),
        },
      }],
    })
  );

  // ── Prompt 3: debug-assist ────────────────────────────────────────────────
  server.prompt(
    'debug-assist',
    'Contextual debugging session for an error with optional stack trace.',
    {
      error:       z.string().describe('Error message or exception text'),
      stack:       z.string().optional().describe('Stack trace (optional)'),
      language:    z.string().default('TypeScript').describe('Programming language'),
      context:     z.string().optional().describe('Brief description of what the code is doing'),
    },
    ({ error, stack, language, context }) => ({
      messages: [
        {
          role: 'user' as const,
          content: {
            type: 'text' as const,
            text: [
              `I'm debugging a ${language} error. Please help me diagnose and fix it.`,
              context ? `Context: ${context}` : '',
              '',
              `**Error:** \`\`\`\n${error}\n\`\`\``,
              stack ? `**Stack trace:**\n\`\`\`\n${stack}\n\`\`\`` : '',
              '',
              'Please provide:',
              '1. **Root cause** — what is actually going wrong',
              '2. **Why it happens** — the mechanism',
              '3. **Fix** — exact code change(s)',
              '4. **Prevention** — how to avoid this class of bug in future',
            ].filter(Boolean).join('\n'),
          },
        },
      ],
    })
  );
}src/prompts.ts
Phase 7 — Entry Point & Bootstrap
The index.ts file wires everything together: declare capabilities, register all three primitive families, connect the transport, and start listening.
// src/index.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { registerTools }     from './tools.js';
import { registerResources } from './resources.js';
import { registerPrompts }   from './prompts.js';

const server = new McpServer(
  {
    name:    'devdash-mcp',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools:     { listChanged: false },
      resources: { subscribe: false, listChanged: false },
      prompts:   { listChanged: false },
      sampling:  {},                     // required for create_pr_draft
      logging:   {},
    },
  }
);

// Register all primitives
registerTools(server);
registerResources(server);
registerPrompts(server);

// Connect and start
const transport = new StdioServerTransport();
await server.connect(transport);

console.error('[DevDash MCP] Server started — listening on stdio');src/index.ts
⚠️
Console.error not console.log: The MCP SDK uses stdout for the JSON-RPC wire protocol. Any logging you write must go to stderr (console.error) or you will corrupt the protocol stream.
Phase 8 — Connecting to Claude Desktop
Build the project and drop one JSON block into the Claude Desktop config. You'll have the full DevDash server talking to Claude within minutes.
1
Build the project
~5 sec
npm run build
# Outputs to dist/  (dist/index.js, dist/tools.js, …)bash
2
Find the config file
location
Open the Claude Desktop config — location varies by OS:
OSPath
macOS~/Library/Application Support/Claude/claude_desktop_config.json
Windows%APPDATA%\Claude\claude_desktop_config.json
Linux~/.config/Claude/claude_desktop_config.json
3
Add the server entry
edit JSON
{
  "mcpServers": {
    "devdash": {
      "command": "node",
      "args": ["/absolute/path/to/devdash-mcp/dist/index.js"],
      "env": {
        "GITHUB_TOKEN": "ghp_YOUR_TOKEN_HERE"
      }
    }
  }
}claude_desktop_config.json
4
Restart Claude Desktop
~5 sec
Quit and reopen Claude Desktop. You should see the 🔧 tools indicator appear in the chat input area. Click it to verify all four tools, five resources, and three prompts are listed.
Phase 9 — Testing Your Server
Three layers of testing: quick smoke tests inside Claude Desktop, CLI inspection, and a lightweight Vitest unit suite for your pure functions.
💬
Claude Desktop Smoke Test
Ask Claude to run each capability directly. These prompts cover every feature.
⌨️
MCP Inspector CLI
Use npx @modelcontextprotocol/inspector to introspect capabilities without a full host.
🧪
Vitest Unit Tests
Test pure functions (complexity analyser, error paths) without spinning up the real server.
Smoke-test prompts to try in Claude Desktop:
# Test search_repos
"Search GitHub for MCP TypeScript servers with more than 100 stars"

# Test get_file_content
"Fetch the README from modelcontextprotocol/typescript-sdk"

# Test analyze_complexity + get_file_content together
"Fetch src/index.ts from modelcontextprotocol/typescript-sdk
 and analyse its cyclomatic complexity"

# Test code-review prompt
"Use the code-review prompt on modelcontextprotocol/typescript-sdk,
 file src/server/mcp.ts, focus on readability"

# Test the devdash://status resource
"Read the devdash://status resource and tell me how long the server has been running"

# Test create_pr_draft (sampling)
"Draft a PR for: Added pagination cursor to the search_repos tool
 so results beyond the first 10 can be fetched"smoke tests
Vitest unit test for the complexity analyser:
// tests/complexity.test.ts
import { describe, it, expect } from 'vitest';
import { analyseComplexity } from '../src/complexity.js';

describe('analyseComplexity', () => {
  it('returns score 1 for a trivial function', () => {
    const code = 'function hello() { return "world"; }';
    const { score, rating } = analyseComplexity(code);
    expect(score).toBe(1);
    expect(rating).toBe('low');
  });

  it('counts if and else-if separately', () => {
    const code = `
      function classify(n) {
        if (n < 0) return 'negative';
        else if (n === 0) return 'zero';
        else return 'positive';
      }
    `;
    const { score, breakdown } = analyseComplexity(code);
    expect(breakdown['if']).toBe(1);
    expect(breakdown['else if']).toBe(1);
    expect(score).toBe(3);
  });

  it('rates score 21+ as high', () => {
    // 25 if-statements = score 26
    const code = Array.from({ length: 25 }, (_, i) => `if (x${i}) {}`).join('\n');
    const { rating } = analyseComplexity(code);
    expect(rating).toBe('high');
  });
});tests/complexity.test.ts
Phase 10 — Production Readiness Checklist
Before sharing your server or publishing it to npm, run through this checklist.
🎉
You just shipped a real MCP server. DevDash has more surface area than most open-source MCP servers. It demonstrates every Week 1 concept in a cohesive, working product.
Architecture at a Glance
How all six modules collaborate at runtime — from a Claude question down to a GitHub API call and back.
graph TD A["👤 Claude Desktop (Host)"] -- "JSON-RPC / stdio" --> B["src/index.ts\nMcpServer bootstrap"] B --> C["src/tools.ts\n4 Tools"] B --> D["src/resources.ts\n3 Static + 2 Templates"] B --> E["src/prompts.ts\n3 Prompt Templates"] C --> F["src/github.ts\nAPI Client + Cache"] C --> G["src/complexity.ts\nCC Analyser"] D --> F C -- "sampling/createMessage" --> A F -- "HTTPS" --> H["GitHub REST API"] style A fill:#1e1b4b,stroke:#7c3aed,color:#c4b5fd style B fill:#2d1b69,stroke:#7c3aed,color:#c4b5fd style C fill:#1a2e1a,stroke:#10b981,color:#6ee7b7 style D fill:#0d2a33,stroke:#06b6d4,color:#67e8f9 style E fill:#2d1625,stroke:#ec4899,color:#f9a8d4 style F fill:#1e1a14,stroke:#f59e0b,color:#fde68a style G fill:#1e1a14,stroke:#f59e0b,color:#fde68a style H fill:#111118,stroke:#444,color:#888
🏆
Week 1 Complete!
You've gone from "What is MCP?" to shipping a production-ready server with real GitHub integration, multi-content tools, live resources, and AI-powered prompts — in 7 days.
✓ MCP Architecture ✓ JSON-RPC 2.0 ✓ TypeScript SDK ✓ Tools in Depth ✓ Resources Deep Dive ✓ Prompts & Sampling ✓ Capstone Server
Capstone Check
5 questions covering the full Week 1 build. Score 5/5 and you're ready for Week 2.
Q1In the DevDash server, create_pr_draft calls server.createMessage(). Which JSON-RPC method does this translate to on the wire?
Atools/call
Bprompts/get
Csampling/createMessage
Dresources/read
Q2The github://file/{owner}/{repo}/{+path} URI template uses {+path} instead of {path}. What does the + operator allow?
AMultiple values separated by commas
BSlashes inside the variable (path expression)
CURL-encoded special characters only
DOptional variable — omit if undefined
Q3Your analyze_complexity tool handler does NOT wrap its logic in try/catch. What happens if the analyser throws?
AThe SDK silently swallows the error
BClaude Desktop shows a generic timeout
CThe SDK converts it to a JSON-RPC error response (code -32603)
DThe entire MCP server process crashes
Q4In src/index.ts, why must all debug logging use console.error instead of console.log?
Aconsole.log is slower in Node.js
Bconsole.log writes to stdout, which the MCP SDK uses for JSON-RPC messages, corrupting the protocol stream
CClaude Desktop only reads stderr
DJSON-RPC requires error-level logging by specification
Q5The code-review prompt embeds a resource using type: 'resource' in its first message. What does this tell the MCP client to do?
ADownload the resource and attach it as a file upload
BResolve the resource URI via resources/read and inline the content into the conversation
CPass the URI string to the model as plain text
DSubscribe to resource change notifications
← Previous Day
Day 6: Prompts & Sampling
The third primitive & AI-to-AI calls
Next Week →
Day 8: Transport Deep Dive
HTTP/SSE, WebSockets & transport security