Go beyond toy examples — build production MCP servers that talk to GitHub, Slack, and PostgreSQL with real live data, proper rate-limit handling, secrets management, and integration testing.
// src/shared/base-server.ts — shared bootstrap every integration server uses
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
export function createServer(name: string, version = '1.0.0'): McpServer {
return new McpServer(
{ name, version },
{
capabilities: {
tools: { listChanged: false },
resources: { listChanged: false },
logging: {},
},
}
);
}
export async function startStdio(server: McpServer): Promise<void> {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`[${server.name}] started on stdio`);
}
// src/shared/rate-limiter.ts — bottleneck limiter factory
import Bottleneck from 'bottleneck';
export function createLimiter(options: {
requestsPerSecond: number;
maxConcurrent: number;
}): Bottleneck {
return new Bottleneck({
minTime: Math.ceil(1000 / options.requestsPerSecond),
maxConcurrent: options.maxConcurrent,
reservoir: options.requestsPerSecond * 10, // burst capacity
reservoirRefreshAmount: options.requestsPerSecond * 10,
reservoirRefreshInterval: 10_000,
});
}src/shared/
// src/github-mcp/index.ts
import { Octokit } from '@octokit/rest';
import { z } from 'zod';
import { createServer, startStdio } from '../shared/base-server.js';
import { createLimiter } from '../shared/rate-limiter.js';
const gh = new Octokit({
auth: process.env.GITHUB_TOKEN,
throttle: { // Octokit built-in retry on 429/5xx
onRateLimit: (retryAfter: number, options: any) => options.request.retryCount < 2,
onSecondaryRateLimit: (_: number, options: any) => options.request.retryCount < 1,
},
});
// 5 req/sec — GitHub REST allows 5000/hr authenticated
const limiter = createLimiter({ requestsPerSecond: 5, maxConcurrent: 3 });
const ghCall = <T>(fn: () => Promise<T>) => limiter.schedule(fn);
const server = createServer('github-mcp');
// ── Tool: search_repositories ────────────────────────────────────────────
server.tool(
'search_repositories',
'Search GitHub repositories with filters. Returns name, description, stars, language, URL.',
{
query: z.string().min(1).describe('GitHub search query (e.g. "mcp typescript stars:>100")'),
sort: z.enum(['stars','forks','updated']).default('stars').describe('Sort order'),
per_page: z.number().int().min(1).max(30).default(10).describe('Results per page (max 30)'),
page: z.number().int().min(1).default(1).describe('Page number for pagination'),
},
async ({ query, sort, per_page, page }) => {
try {
const { data } = await ghCall(() =>
gh.rest.search.repos({ q: query, sort, per_page, page })
);
const lines = [
`Found ${data.total_count.toLocaleString()} repositories (page ${page}):`,
'',
...data.items.map(r =>
`**${r.full_name}** ⭐ ${r.stargazers_count.toLocaleString()}\n` +
` ${r.description ?? 'No description'}\n` +
` Language: ${r.language ?? 'unknown'} | Forks: ${r.forks_count}\n` +
` ${r.html_url}`
),
data.items.length === per_page ? `\nNext page: ${page + 1}` : '\nNo more results.',
];
return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `GitHub search failed: ${err.message}` }] };
}
}
);
// ── Tool: list_issues ─────────────────────────────────────────────────────
server.tool(
'list_issues',
'List issues for a GitHub repository with optional filters.',
{
owner: z.string().describe('Repository owner (user or org)'),
repo: z.string().describe('Repository name'),
state: z.enum(['open','closed','all']).default('open').describe('Issue state filter'),
labels: z.string().optional().describe('Comma-separated label names to filter by'),
limit: z.number().int().min(1).max(50).default(20).describe('Max issues to return'),
},
async ({ owner, repo, state, labels, limit }) => {
try {
const { data } = await ghCall(() =>
gh.rest.issues.listForRepo({
owner, repo, state,
labels: labels ?? undefined,
per_page: limit,
})
);
// Filter out pull requests (GitHub returns them in issues endpoint)
const issues = data.filter(i => !i.pull_request);
const lines = [
`${issues.length} ${state} issue(s) in ${owner}/${repo}:`,
'',
...issues.map(i =>
`#${i.number} [${i.state}] ${i.title}\n` +
` Labels: ${i.labels.map((l: any) => l.name).join(', ') || 'none'}\n` +
` Opened: ${new Date(i.created_at).toDateString()} by ${i.user?.login}\n` +
` ${i.html_url}`
),
];
return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `list_issues failed: ${err.message}` }] };
}
}
);
// ── Tool: create_issue ────────────────────────────────────────────────────
server.tool(
'create_issue',
'Create a new issue in a GitHub repository.',
{
owner: z.string().describe('Repository owner'),
repo: z.string().describe('Repository name'),
title: z.string().min(1).max(256).describe('Issue title'),
body: z.string().optional().describe('Issue body in Markdown'),
labels: z.array(z.string()).optional().describe('Label names to apply'),
},
{ readOnlyHint: false, destructiveHint: false },
async ({ owner, repo, title, body, labels }) => {
try {
const { data } = await ghCall(() =>
gh.rest.issues.create({ owner, repo, title, body, labels })
);
return {
content: [{
type: 'text' as const,
text: `Issue created: #${data.number} — ${data.title}\n${data.html_url}`,
}],
};
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `create_issue failed: ${err.message}` }] };
}
}
);
// ── Tool: get_pull_request ────────────────────────────────────────────────
server.tool(
'get_pull_request',
'Fetch full details of a pull request including diff stats and review status.',
{
owner: z.string().describe('Repository owner'),
repo: z.string().describe('Repository name'),
pr_number: z.number().int().min(1).describe('Pull request number'),
},
async ({ owner, repo, pr_number }) => {
try {
const [{ data: pr }, { data: reviews }] = await Promise.all([
ghCall(() => gh.rest.pulls.get({ owner, repo, pull_number: pr_number })),
ghCall(() => gh.rest.pulls.listReviews({ owner, repo, pull_number: pr_number })),
]);
const summary = [
`PR #${pr.number}: ${pr.title}`,
`State: ${pr.state} | Draft: ${pr.draft ? 'yes' : 'no'}`,
`Author: ${pr.user?.login} | Base: ${pr.base.ref} ← ${pr.head.ref}`,
`+${pr.additions} / -${pr.deletions} in ${pr.changed_files} file(s)`,
`Reviews: ${reviews.length} (${reviews.filter(r => r.state === 'APPROVED').length} approved)`,
`Mergeable: ${pr.mergeable ?? 'unknown'}`,
'',
pr.body ? `## Description\n${pr.body.slice(0, 500)}${pr.body.length > 500 ? '…' : ''}` : 'No description.',
'',
pr.html_url,
];
return { content: [{ type: 'text' as const, text: summary.join('\n') }] };
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `get_pull_request failed: ${err.message}` }] };
}
}
);
await startStdio(server);src/github-mcp/index.ts
403 secondary rate limits automatically. Combined with Bottleneck's limiter, you get defense in depth — Bottleneck prevents hitting the limit, Octokit retries when you do. The two layers complement each other.@slack/web-api client handles retries automatically; you focus on building useful tools.channels:read, channels:history, chat:write, files:write, search:read, users:read. Add them in your Slack App manifest.// src/slack-mcp/index.ts
import { WebClient } from '@slack/web-api';
import { z } from 'zod';
import { createServer, startStdio } from '../shared/base-server.js';
import { createLimiter } from '../shared/rate-limiter.js';
const slack = new WebClient(process.env.SLACK_BOT_TOKEN, {
retryConfig: { retries: 3 }, // auto-retry on 429 and 5xx
});
// Tier 3 methods: 50 req/min → ~0.8 req/sec with headroom
const limiter = createLimiter({ requestsPerSecond: 0.8, maxConcurrent: 2 });
const slackCall = <T>(fn: () => Promise<T>) => limiter.schedule(fn);
const server = createServer('slack-mcp');
// ── Tool: list_channels ───────────────────────────────────────────────────
server.tool(
'list_channels',
'List public Slack channels with member counts and topics.',
{
limit: z.number().int().min(1).max(100).default(20).describe('Max channels to return'),
cursor: z.string().optional().describe('Pagination cursor from previous call'),
exclude_archived: z.boolean().default(true).describe('Skip archived channels'),
},
async ({ limit, cursor, exclude_archived }) => {
try {
const result = await slackCall(() =>
slack.conversations.list({
limit,
cursor,
exclude_archived,
types: 'public_channel',
})
);
if (!result.ok) throw new Error(result.error);
const channels = result.channels ?? [];
const lines = [
`${channels.length} channel(s):`,
'',
...channels.map((c: any) =>
`#${c.name} (${c.num_members} members)\n Topic: ${c.topic?.value || 'none'}\n ID: ${c.id}`
),
result.response_metadata?.next_cursor
? `\nNext cursor: ${result.response_metadata.next_cursor}`
: '\nNo more channels.',
];
return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `list_channels failed: ${err.message}` }] };
}
}
);
// ── Tool: send_message ────────────────────────────────────────────────────
server.tool(
'send_message',
'Send a message to a Slack channel or DM. Supports Markdown via Slack mrkdwn.',
{
channel: z.string().describe('Channel ID (C01234) or user ID (U01234) for DM'),
text: z.string().min(1).max(4000).describe('Message text (Slack mrkdwn supported)'),
thread_ts: z.string().optional().describe('Thread timestamp to reply in a thread'),
unfurl_links: z.boolean().default(false).describe('Whether to unfurl link previews'),
},
{ readOnlyHint: false, destructiveHint: false, idempotentHint: false },
async ({ channel, text, thread_ts, unfurl_links }) => {
try {
const result = await slackCall(() =>
slack.chat.postMessage({ channel, text, thread_ts, unfurl_links })
);
if (!result.ok) throw new Error(result.error);
return {
content: [{
type: 'text' as const,
text: `Message sent ✓\nChannel: ${channel}\nTimestamp: ${result.ts}\nPermalink: https://slack.com/archives/${channel}/p${result.ts?.replace('.', '')}`,
}],
};
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `send_message failed: ${err.message}` }] };
}
}
);
// ── Tool: search_messages ─────────────────────────────────────────────────
server.tool(
'search_messages',
'Full-text search across Slack messages the bot can access.',
{
query: z.string().min(1).describe('Search query (supports Slack search modifiers: in:channel, from:user, before:date)'),
count: z.number().int().min(1).max(20).default(10).describe('Number of results'),
sort: z.enum(['score','timestamp']).default('score').describe('Result ordering'),
},
async ({ query, count, sort }) => {
try {
const result = await slackCall(() =>
slack.search.messages({ query, count, sort })
);
if (!result.ok) throw new Error(result.error);
const messages = result.messages?.matches ?? [];
const lines = [
`Found ${result.messages?.total ?? 0} message(s), showing ${messages.length}:`,
'',
...messages.map((m: any) =>
`[${new Date(Number(m.ts) * 1000).toISOString().slice(0,10)}] #${m.channel?.name ?? m.channel?.id}\n` +
` ${m.username ?? m.user}: ${m.text?.slice(0, 200)}\n` +
` ${m.permalink}`
),
];
return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `search_messages failed: ${err.message}` }] };
}
}
);
// ── Tool: get_user_info ───────────────────────────────────────────────────
server.tool(
'get_user_info',
'Look up a Slack user by email address or user ID.',
{
identifier: z.string().describe('Slack user ID (U01234…) or email address'),
},
async ({ identifier }) => {
try {
const result = await slackCall(() =>
identifier.includes('@')
? slack.users.lookupByEmail({ email: identifier })
: slack.users.info({ user: identifier })
);
if (!result.ok) throw new Error((result as any).error);
const user = (result as any).user;
return {
content: [{
type: 'text' as const,
text: [
`User: ${user.real_name} (@${user.name})`,
`ID: ${user.id}`,
`Email: ${user.profile?.email ?? 'hidden'}`,
`Title: ${user.profile?.title || 'none'}`,
`Timezone: ${user.tz}`,
`Status: ${user.profile?.status_emoji} ${user.profile?.status_text}`,
`Deleted: ${user.deleted} | Bot: ${user.is_bot}`,
].join('\n'),
}],
};
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `get_user_info failed: ${err.message}` }] };
}
}
);
await startStdio(server);src/slack-mcp/index.ts
DELETE without explicit intent.$1, $2, …) — never string-concatenate user input into SQL. The query_database tool only accepts read statements; write operations require an explicit separate tool with destructive annotation.// src/pg-mcp/index.ts
import pg from 'pg';
import { z } from 'zod';
import { createServer, startStdio } from '../shared/base-server.js';
const { Pool } = pg;
// Connection pool — max 5 connections (be conservative with DB resources)
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 5,
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
ssl: process.env.DATABASE_SSL === 'true' ? { rejectUnauthorized: false } : false,
});
// Verify connection on startup
pool.query('SELECT 1').then(() => {
console.error('[pg-mcp] Database connected ✓');
}).catch(err => {
console.error('[pg-mcp] Database connection failed:', err.message);
process.exit(1);
});
const server = createServer('pg-mcp');
// ── Tool: list_tables ─────────────────────────────────────────────────────
server.tool(
'list_tables',
'List all user-defined tables and views in the connected database with row estimates.',
{
schema: z.string().default('public').describe('PostgreSQL schema name'),
},
{ readOnlyHint: true },
async ({ schema }) => {
try {
const { rows } = await pool.query(
`SELECT
table_name,
table_type,
pg_size_pretty(pg_total_relation_size(quote_ident(table_schema) || '.' || quote_ident(table_name))) AS size,
(SELECT reltuples::bigint FROM pg_class WHERE relname = table_name) AS row_estimate
FROM information_schema.tables
WHERE table_schema = $1
AND table_type IN ('BASE TABLE', 'VIEW')
ORDER BY table_name`,
[schema]
);
const lines = [
`${rows.length} table(s)/view(s) in schema "${schema}":`,
'',
...rows.map((r: any) =>
`${r.table_type === 'VIEW' ? '👁' : '📋'} ${r.table_name} ~${Number(r.row_estimate).toLocaleString()} rows ${r.size}`
),
];
return { content: [{ type: 'text' as const, text: lines.join('\n') }] };
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `list_tables failed: ${err.message}` }] };
}
}
);
// ── Tool: describe_table ──────────────────────────────────────────────────
server.tool(
'describe_table',
'Show columns, types, constraints, and indexes for a table.',
{
table: z.string().describe('Table name'),
schema: z.string().default('public').describe('Schema name'),
},
{ readOnlyHint: true },
async ({ table, schema }) => {
try {
const { rows: cols } = await pool.query(
`SELECT column_name, data_type, character_maximum_length,
is_nullable, column_default
FROM information_schema.columns
WHERE table_schema = $1 AND table_name = $2
ORDER BY ordinal_position`,
[schema, table]
);
const { rows: idxs } = await pool.query(
`SELECT indexname, indexdef
FROM pg_indexes
WHERE schemaname = $1 AND tablename = $2`,
[schema, table]
);
const colLines = cols.map((c: any) =>
` ${c.column_name.padEnd(28)} ${c.data_type}${c.character_maximum_length ? `(${c.character_maximum_length})` : ''}${c.is_nullable === 'NO' ? ' NOT NULL' : ''}${c.column_default ? ` DEFAULT ${c.column_default}` : ''}`
);
const idxLines = idxs.map((i: any) => ` ${i.indexname}: ${i.indexdef.replace(/.*USING/, 'USING')}`);
return {
content: [{
type: 'text' as const,
text: [
`Table: ${schema}.${table}`,
`\nColumns (${cols.length}):`,
...colLines,
idxLines.length ? `\nIndexes (${idxLines.length}):` : '',
...idxLines,
].filter(Boolean).join('\n'),
}],
};
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `describe_table failed: ${err.message}` }] };
}
}
);
// ── Tool: query_database ──────────────────────────────────────────────────
server.tool(
'query_database',
'Execute a SELECT query against the database. READ-ONLY — no INSERT/UPDATE/DELETE.',
{
sql: z.string().describe('SQL SELECT statement (parameterized $1,$2 placeholders allowed)'),
params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()]))
.default([])
.describe('Parameter values for $1, $2, … placeholders'),
limit: z.number().int().min(1).max(500).default(50).describe('Hard cap on rows returned'),
},
{ readOnlyHint: true },
async ({ sql, params, limit }) => {
// Reject non-SELECT statements at the tool layer
const normalized = sql.trim().toLowerCase();
if (!/^(select|with|explain)\b/.test(normalized)) {
return {
isError: true,
content: [{ type: 'text' as const, text: 'Only SELECT, WITH, and EXPLAIN statements are allowed in query_database. Use execute_statement for writes.' }],
};
}
try {
// Wrap in a read-only transaction for extra safety
const client = await pool.connect();
try {
await client.query('BEGIN READ ONLY');
// Enforce row limit by wrapping in a subquery
const limitedSql = `SELECT * FROM (${sql}) _q LIMIT $${params.length + 1}`;
const { rows, fields } = await client.query(limitedSql, [...params, limit]);
await client.query('COMMIT');
const header = fields.map((f: any) => f.name).join(' | ');
const separator = '-'.repeat(header.length);
const dataRows = rows.map((r: any) =>
fields.map((f: any) => String(r[f.name] ?? 'NULL')).join(' | ')
);
return {
content: [{
type: 'text' as const,
text: [
`${rows.length} row(s) returned:`,
header, separator, ...dataRows,
rows.length === limit ? `\n⚠ Row limit (${limit}) reached — results may be truncated.` : '',
].filter(Boolean).join('\n'),
}],
};
} finally {
client.release();
}
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `query_database failed: ${err.message}` }] };
}
}
);
// ── Tool: execute_statement ───────────────────────────────────────────────
server.tool(
'execute_statement',
'Execute a write SQL statement (INSERT/UPDATE/DELETE). DESTRUCTIVE — use with caution.',
{
sql: z.string().describe('SQL statement with $1,$2 placeholders'),
params: z.array(z.union([z.string(), z.number(), z.boolean(), z.null()])).default([]),
},
{ readOnlyHint: false, destructiveHint: true, idempotentHint: false },
async ({ sql, params }) => {
const normalized = sql.trim().toLowerCase();
// Block DDL — no DROP, ALTER, TRUNCATE from AI
if (/\b(drop|alter|truncate|create|grant|revoke)\b/.test(normalized)) {
return {
isError: true,
content: [{ type: 'text' as const, text: 'DDL statements (DROP/ALTER/TRUNCATE/CREATE) are not permitted.' }],
};
}
try {
const { rowCount, command } = await pool.query(sql, params);
return {
content: [{
type: 'text' as const,
text: `${command} completed. Rows affected: ${rowCount}`,
}],
};
} catch (err: any) {
return { isError: true, content: [{ type: 'text' as const, text: `execute_statement failed: ${err.message}` }] };
}
}
);
// Graceful shutdown — release pool connections
process.on('SIGTERM', async () => { await pool.end(); process.exit(0); });
await startStdio(server);src/pg-mcp/index.ts
| API | Default limit | Authenticated limit | Rate-limit error | Retry after | Risk |
|---|---|---|---|---|---|
| GitHub REST | 60 req/hr unauthenticated | 5,000 req/hr (PAT) | HTTP 429 + X-RateLimit-Reset |
X-RateLimit-Reset header (Unix ts) |
LOW |
| GitHub Secondary | – | Concurrent request limit | HTTP 403 + message body | Retry-After header (seconds) |
MED |
| Slack Tier 1 | – | 1 req/min (heavy methods) | HTTP 429 {"ok":false,"error":"ratelimited"} |
Retry-After header |
HIGH |
| Slack Tier 2 | – | 20 req/min | HTTP 429 | Retry-After header |
MED |
| Slack Tier 3 | – | 50 req/min (most read methods) | HTTP 429 | Retry-After header |
LOW |
| PostgreSQL | max_connections (default 100) | Set by pool size (use max 5–10) | pg error: too many connections | Pool queue (automatic) | MED |
reservoir pattern gives you burst capacity (short bursts above the sustained rate) while staying within hourly limits. Set reservoir = requestsPerSecond × 10 for 10-second burst tolerance and reservoirRefreshInterval = 10_000 to refill every 10 seconds.// src/shared/secrets.ts — typed secrets loader with validation
import { z } from 'zod';
// ── GitHub MCP secrets ────────────────────────────────────────────────────
const githubSecrets = z.object({
GITHUB_TOKEN: z.string().min(1, 'GITHUB_TOKEN is required'),
});
// ── Slack MCP secrets ─────────────────────────────────────────────────────
const slackSecrets = z.object({
SLACK_BOT_TOKEN: z.string().startsWith('xoxb-', 'Must be a Slack Bot Token (xoxb-)'),
SLACK_APP_TOKEN: z.string().startsWith('xapp-', 'App-level token for Socket Mode').optional(),
});
// ── PostgreSQL MCP secrets ────────────────────────────────────────────────
const pgSecrets = z.object({
DATABASE_URL: z.string().url('DATABASE_URL must be a valid PostgreSQL connection string'),
DATABASE_SSL: z.enum(['true','false']).default('false'),
});
export type ServerType = 'github' | 'slack' | 'pg';
export function loadSecrets(type: ServerType) {
const schemas = { github: githubSecrets, slack: slackSecrets, pg: pgSecrets };
const result = schemas[type].safeParse(process.env);
if (!result.success) {
console.error(`[${type}-mcp] Missing secrets:\n${result.error.issues.map(i => ` ✗ ${i.path.join('.')}: ${i.message}`).join('\n')}`);
process.exit(1);
}
return result.data;
}
// Call at startup — fails fast with clear error messages
// const secrets = loadSecrets('github');
// const { GITHUB_TOKEN } = secrets;src/shared/secrets.ts
# .env.github
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# .env.slack
SLACK_BOT_TOKEN=xoxb-000000000000-000000000000-xxxxxxxxxxxxxxxxxxxxxxxx
# .env.pg
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
DATABASE_SSL=false
# Claude Desktop config — separate server per integration
# claude_desktop_config.json
{
"mcpServers": {
"github": {
"command": "node",
"args": ["./src/github-mcp/dist/index.js"],
"env": { "GITHUB_TOKEN": "ghp_your_token_here" }
},
"slack": {
"command": "node",
"args": ["./src/slack-mcp/dist/index.js"],
"env": { "SLACK_BOT_TOKEN": "xoxb-your-token-here" }
},
"pg": {
"command": "node",
"args": ["./src/pg-mcp/dist/index.js"],
"env": { "DATABASE_URL": "postgresql://localhost/mydb" }
}
}
}env + config
// src/composite/dev-briefing.ts
// A "meta" tool that aggregates data from GitHub + Slack + Postgres
// This runs inside a composite server that connects to all three
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { z } from 'zod';
import { createServer, startStdio } from '../shared/base-server.js';
// Pre-connected clients (created at startup)
declare const githubClient: Client;
declare const slackClient: Client;
declare const pgClient: Client;
const server = createServer('composite-mcp');
server.tool(
'get_incident_briefing',
'Aggregate incident context: open GitHub issues + Slack #incidents messages + affected-user count from DB.',
{
label: z.string().describe('GitHub issue label (e.g. "bug", "incident")'),
channel: z.string().describe('Slack channel ID for incident discussion'),
since: z.string().describe('ISO date — look back from this date (e.g. 2026-04-01)'),
},
async ({ label, channel, since }) => {
// Fan out to three APIs concurrently
const [issuesResult, messagesResult, dbResult] = await Promise.allSettled([
githubClient.callTool({
name: 'list_issues',
arguments: { owner: 'myorg', repo: 'myapp', state: 'open', labels: label },
}),
slackClient.callTool({
name: 'search_messages',
arguments: { query: `in:${channel} after:${since}`, count: 20 },
}),
pgClient.callTool({
name: 'query_database',
arguments: {
sql: `SELECT COUNT(*) AS affected FROM users WHERE last_error_at > $1`,
params: [since],
},
}),
]);
const sections: string[] = ['## Incident Briefing\n'];
// GitHub issues
if (issuesResult.status === 'fulfilled' && !issuesResult.value.isError) {
sections.push('### Open Issues\n' + (issuesResult.value.content[0] as any).text);
} else {
sections.push('### Open Issues\n⚠ Failed to fetch GitHub issues.');
}
// Slack messages
if (messagesResult.status === 'fulfilled' && !messagesResult.value.isError) {
sections.push('\n### Slack Discussion\n' + (messagesResult.value.content[0] as any).text);
} else {
sections.push('\n### Slack Discussion\n⚠ Failed to fetch Slack messages.');
}
// DB count
if (dbResult.status === 'fulfilled' && !dbResult.value.isError) {
sections.push('\n### Affected Users\n' + (dbResult.value.content[0] as any).text);
} else {
sections.push('\n### Affected Users\n⚠ Failed to query database.');
}
return { content: [{ type: 'text' as const, text: sections.join('\n') }] };
}
);
await startStdio(server);src/composite/dev-briefing.ts
Promise.allSettled is essential for aggregation. Unlike Promise.all, it never rejects — each API call either fulfills or fails independently. You always get a result with partial data rather than a total failure if one API is down. Always use allSettled for cross-API fan-out.// tests/github-mcp.test.ts — test with mocked Octokit
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { createInProcessTransportPair } from '../src/shared/in-process-transport.js';
// Mock Octokit before importing the server
vi.mock('@octokit/rest', () => ({
Octokit: vi.fn().mockImplementation(() => ({
rest: {
search: {
repos: vi.fn().mockResolvedValue({
data: {
total_count: 2,
items: [
{ full_name: 'org/repo-a', description: 'Test repo A', stargazers_count: 100,
language: 'TypeScript', forks_count: 10, html_url: 'https://github.com/org/repo-a' },
{ full_name: 'org/repo-b', description: 'Test repo B', stargazers_count: 50,
language: 'JavaScript', forks_count: 5, html_url: 'https://github.com/org/repo-b' },
],
},
}),
},
issues: {
listForRepo: vi.fn().mockResolvedValue({
data: [
{ number: 42, state: 'open', title: 'Bug: crash on startup',
labels: [{ name: 'bug' }], created_at: '2026-01-01T00:00:00Z',
user: { login: 'alice' }, html_url: 'https://github.com/org/repo/issues/42',
pull_request: undefined },
],
}),
},
},
})),
}));
describe('GitHub MCP Server', () => {
let client: Client;
beforeEach(async () => {
const [clientTransport, serverTransport] = createInProcessTransportPair();
const { buildGitHubServer } = await import('../src/github-mcp/server.js');
const server = buildGitHubServer();
await server.connect(serverTransport);
client = new Client({ name: 'test', version: '1.0.0' }, { capabilities: {} });
await client.connect(clientTransport);
});
it('search_repositories returns formatted results', async () => {
const result = await client.callTool({
name: 'search_repositories',
arguments: { query: 'mcp typescript', sort: 'stars', per_page: 10, page: 1 },
});
expect(result.isError).toBeFalsy();
const text = (result.content[0] as any).text;
expect(text).toContain('2 repositories');
expect(text).toContain('org/repo-a');
expect(text).toContain('⭐ 100');
});
it('list_issues excludes pull requests', async () => {
const result = await client.callTool({
name: 'list_issues',
arguments: { owner: 'org', repo: 'repo', state: 'open', limit: 20 },
});
expect(result.isError).toBeFalsy();
const text = (result.content[0] as any).text;
expect(text).toContain('#42');
expect(text).toContain('Bug: crash on startup');
});
});tests/github-mcp.test.ts
notifications/resources/updated, your MCP clients receive live updates whenever a PR is merged or a Slack message arrives in a watched channel.// src/webhook-bridge/index.ts
// Bridge GitHub webhooks → MCP resource update notifications
import express from 'express';
import { createHmac } from 'crypto';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { (req as any).rawBody = buf; } }));
// Active SSE sessions
const sessions = new Map<string, SSEServerTransport>();
// Track subscribed resources per session
const subscriptions = new Map<string, Set<string>>();
function buildMcpServer(): McpServer {
const server = new McpServer(
{ name: 'webhook-bridge', version: '1.0.0' },
{ capabilities: { resources: { subscribe: true, listChanged: false } } }
);
// Expose repo events as subscribable resources
server.resource(
'github-pr-events',
new ResourceTemplate('github://events/{owner}/{repo}/prs', { list: undefined }),
{ mimeType: 'application/json' },
async (uri, { owner, repo }) => {
// Return current open PRs as initial content
const data = { owner, repo, message: 'Subscribe to receive live PR events' };
return { contents: [{ uri: uri.href, mimeType: 'application/json', text: JSON.stringify(data) }] };
}
);
return server;
}
// GitHub webhook endpoint
app.post('/webhook/github', (req, res) => {
// Verify HMAC signature
const sig = req.headers['x-hub-signature-256'] as string ?? '';
const hmac = createHmac('sha256', process.env.GITHUB_WEBHOOK_SECRET ?? '');
hmac.update((req as any).rawBody);
const expected = `sha256=${hmac.digest('hex')}`;
if (sig !== expected) { res.status(401).send('Invalid signature'); return; }
const event = req.headers['x-github-event'] as string;
const payload = req.body;
if (event === 'pull_request') {
const uri = `github://events/${payload.repository.owner.login}/${payload.repository.name}/prs`;
// Notify all sessions subscribed to this resource URI
sessions.forEach((transport, sessionId) => {
if (subscriptions.get(sessionId)?.has(uri)) {
transport.send({
jsonrpc: '2.0',
method: 'notifications/resources/updated',
params: { uri },
});
}
});
}
res.status(200).send('ok');
});
app.listen(3000, () => console.error('[webhook-bridge] listening on :3000'));src/webhook-bridge/index.ts
interface GitHubAdapter). Swap real clients for mocks in tests with zero impact on tool handlers. The adapter owns rate limiting, retry, and auth.$1, $2 placeholders always.destructiveHint: true. Hosts use this to show confirmation dialogs.isError: true on failure — never throw to the protocol layer from expected API errors (404, 403, rate limit).vi.mock or nock to intercept HTTP.query_database tool wraps the SELECT in BEGIN READ ONLY … COMMIT. What does a read-only transaction add beyond just running the SELECT directly?reservoir pattern is set to requestsPerSecond × 10. What behaviour does this enable?Promise.allSettled instead of Promise.all in the get_incident_briefing aggregation tool. Why?Promise.allSettled is faster because it doesn't wait for slow promisesPromise.allSettled always resolves even if some promises reject, letting you return partial results when one API is down instead of failing the entire briefingPromise.allSettled retries rejected promises automaticallyPromise.all doesn't work with async/await in tool handlersexecute_statement tool blocks DDL keywords (DROP, ALTER, TRUNCATE) even though it's a write tool. Why not just rely on database-level permissions?