mcp-patterns
Core MCP SDK patterns for TypeScript/Node.js server implementation, tool registration, and resource handling
$ npx -y skills add notque/vexjoy-agent --agent claude-codeHow it fires
How this agent gets triggered: by you, by Claude, or both.
- Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
- You can call itInvoke it directly when you want it.
Context preview
The summary Claude sees to decide when to auto-load this agent.
Core MCP SDK patterns for TypeScript/Node.js server implementation, tool registration, and resource handling
Agent definition
mcp-patterns.mddescription: Core MCP SDK patterns for TypeScript/Node.js server implementation, tool registration, and resource handling
MCP Server Development Patterns
> **Scope**: TypeScript/Node.js MCP via `@modelcontextprotocol/sdk` 0.5.0+.
Pattern Table
| Pattern | Version | Use When | Avoid When | |---------|---------|----------|------------| | `StdioServerTransport` | 0.5.0+ | Claude Desktop integration | HTTP API needed | | `SSEServerTransport` | 0.5.0+ | Web-based clients, multiple connections | Single-process use | | `server.setRequestHandler()` | 0.5.0+ | All request handling | Direct protocol manipulation | | `ListResourcesRequestSchema` | 0.5.0+ | Exposing documents/files | Structured data with tools | | `CallToolRequestSchema` | 0.5.0+ | Search, filtering, actions | Read-only resource access |
---
Correct Patterns
Tool Registration with Input Schema
Explicit JSON Schema for client-side validation and LLM parameter guidance.
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
const SearchSchema = z.object({
query: z.string().min(1).describe('Search terms'),
limit: z.number().int().min(1).max(100).default(10).describe('Max results'),
tags: z.array(z.string()).optional().describe('Filter by tags'),
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'search_docs') {
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
// Validate with Zod before processing
const args = SearchSchema.parse(request.params.arguments ?? {});
const results = await searchIndex(args.query, args.limit, args.tags);
return {
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
};
});**Why**: Without schema, malformed arguments cause cryptic errors instead of `InvalidParams`.
---
Resource URI Design with Custom Scheme
`docs://` scheme — never expose filesystem paths.
function pathToUri(docsRoot: string, filePath: string): string {
const relative = path.relative(docsRoot, filePath);
// docs://guides/api-reference.md — portable, no host path leakage
return `docs://${relative.replace(path.sep, '/')}`;
}
function uriToPath(docsRoot: string, uri: string): string {
if (!uri.startsWith('docs://')) {
throw new McpError(ErrorCode.InvalidParams, `Invalid URI scheme: ${uri}`);
}
const relative = uri.slice('docs://'.length);
// Prevent path traversal
const resolved = path.resolve(docsRoot, relative);
if (!resolved.startsWith(docsRoot)) {
throw new McpError(ErrorCode.InvalidParams, 'Path traversal not allowed');
}
return resolved;
}**Why**: `file:///` exposes filesystem layout. `docs://` is portable.
---
Startup Indexing with Background Continuation
Serve partial results for large corpora while indexing continues.
class DocsServer {
private indexReady = false;
private indexingPromise: Promise<void> | null = null;
async start(): Promise<void> {
// Begin indexing, but don't await completion — serve partial results
this.indexingPromise = this.indexDocs();
// Wait up to 10s for initial batch, then proceed
await Promise.race([
this.indexingPromise,
new Promise<void>((resolve) => setTimeout(resolve, 10_000)),
]);
this.indexReady = true;
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
private async indexDocs(): Promise<void> {
const files = await glob('**/*.md', { cwd: this.docsPath });
// Process in batches of 50 to avoid memory spikes
for (let i = 0; i < files.length; i += 50) {
const batch = files.slice(i, i + 50);
await Promise.all(batch.map((f) => this.parseAndCache(f)));
}
}
}**Why**: Full indexing before connect causes client timeouts. Batch processing prevents heap exhaustion.
---
Pattern Catalog
Use Async File I/O in Request Handlers
**Detection**:
grep -rn 'readFileSync\|existsSync\|readdirSync' --include="*.ts" src/
rg 'Sync\(' --type ts src/**Signal**:
server.setRequestHandler(ReadResourceRequestSchema, (request) => {
// NOT async — blocks the entire Node.js event loop
const content = fs.readFileSync(uriToPath(request.params.uri), 'utf-8');
return { contents: [{ uri: request.params.uri, text: content }] };
});**Why this matters**: `readFileSync` blocks the event loop. While one client is reading a large file, all other clients are frozen. Under concurrent load this causes cascading timeouts — the MCP client kills the connection thinking the server hung.
**Preferred action**:
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const filePath = uriToPath(this.docsRoot, request.params.uri);
const content = await fs.promises.readFile(filePath, 'utf-8');
return { contents: [{ uri: request.params.uri, text: content }] };
});---
Index Once at Startup and Serve from Cache
**Detection**:
grep -rn 'indexDocs\|parseDoc\|WalkDir' --include="*.ts" src/
# Flag if found inside a setRequestHandler callback
**Signal**:
server.setRequestHandler(ListResourcesRequestSchema, async () => {
// Re-parses all markdown files on every list call — seconds of delay
const docs = await this.indexDocs();
return { resources: docs.map((d) => ({ uri: d.uri, name: d.metadata.title })) };
});**Why this matters**: Claude calls `resources/list` repeatedly during a session. Re-parsing 1000 files takes 3-10 seconds each time. The LLM context window fills with timeout errors before any content arrives.
**Preferred action**: Index once at startup, serve from in-memory Map, use mtime-based cache invalidation:
private docsIndex = new Map<string, ParsedDoc>();
// Called once at startup
async indexDocs(): Promise<void> {
const files = await glob('**/*.md', { cwd: this.docsPath });Read more
description: Core MCP SDK patterns for TypeScript/Node.js server implementation, tool registration, and resource handling
MCP Server Development Patterns
> **Scope**: TypeScript/Node.js MCP via `@modelcontextprotocol/sdk` 0.5.0+.
Pattern Table
| Pattern | Version | Use When | Avoid When | |---------|---------|----------|------------| | `StdioServerTransport` | 0.5.0+ | Claude Desktop integration | HTTP API needed | | `SSEServerTransport` | 0.5.0+ | Web-based clients, multiple connections | Single-process use | | `server.setRequestHandler()` | 0.5.0+ | All request handling | Direct protocol manipulation | | `ListResourcesRequestSchema` | 0.5.0+ | Exposing documents/files | Structured data with tools | | `CallToolRequestSchema` | 0.5.0+ | Search, filtering, actions | Read-only resource access |
---
Correct Patterns
Tool Registration with Input Schema
Explicit JSON Schema for client-side validation and LLM parameter guidance.
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
const SearchSchema = z.object({
query: z.string().min(1).describe('Search terms'),
limit: z.number().int().min(1).max(100).default(10).describe('Max results'),
tags: z.array(z.string()).optional().describe('Filter by tags'),
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name !== 'search_docs') {
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
}
// Validate with Zod before processing
const args = SearchSchema.parse(request.params.arguments ?? {});
const results = await searchIndex(args.query, args.limit, args.tags);
return {
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
};
});**Why**: Without schema, malformed arguments cause cryptic errors instead of `InvalidParams`.
---
Resource URI Design with Custom Scheme
`docs://` scheme — never expose filesystem paths.
function pathToUri(docsRoot: string, filePath: string): string {
const relative = path.relative(docsRoot, filePath);
// docs://guides/api-reference.md — portable, no host path leakage
return `docs://${relative.replace(path.sep, '/')}`;
}
function uriToPath(docsRoot: string, uri: string): string {
if (!uri.startsWith('docs://')) {
throw new McpError(ErrorCode.InvalidParams, `Invalid URI scheme: ${uri}`);
}
const relative = uri.slice('docs://'.length);
// Prevent path traversal
const resolved = path.resolve(docsRoot, relative);
if (!resolved.startsWith(docsRoot)) {
throw new McpError(ErrorCode.InvalidParams, 'Path traversal not allowed');
}
return resolved;
}**Why**: `file:///` exposes filesystem layout. `docs://` is portable.
---
Startup Indexing with Background Continuation
Serve partial results for large corpora while indexing continues.
class DocsServer {
private indexReady = false;
private indexingPromise: Promise<void> | null = null;
async start(): Promise<void> {
// Begin indexing, but don't await completion — serve partial results
this.indexingPromise = this.indexDocs();
// Wait up to 10s for initial batch, then proceed
await Promise.race([
this.indexingPromise,
new Promise<void>((resolve) => setTimeout(resolve, 10_000)),
]);
this.indexReady = true;
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
private async indexDocs(): Promise<void> {
const files = await glob('**/*.md', { cwd: this.docsPath });
// Process in batches of 50 to avoid memory spikes
for (let i = 0; i < files.length; i += 50) {
const batch = files.slice(i, i + 50);
await Promise.all(batch.map((f) => this.parseAndCache(f)));
}
}
}**Why**: Full indexing before connect causes client timeouts. Batch processing prevents heap exhaustion.
---
Pattern Catalog
Use Async File I/O in Request Handlers
**Detection**:
grep -rn 'readFileSync\|existsSync\|readdirSync' --include="*.ts" src/
rg 'Sync\(' --type ts src/**Signal**:
server.setRequestHandler(ReadResourceRequestSchema, (request) => {
// NOT async — blocks the entire Node.js event loop
const content = fs.readFileSync(uriToPath(request.params.uri), 'utf-8');
return { contents: [{ uri: request.params.uri, text: content }] };
});**Why this matters**: `readFileSync` blocks the event loop. While one client is reading a large file, all other clients are frozen. Under concurrent load this causes cascading timeouts — the MCP client kills the connection thinking the server hung.
**Preferred action**:
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const filePath = uriToPath(this.docsRoot, request.params.uri);
const content = await fs.promises.readFile(filePath, 'utf-8');
return { contents: [{ uri: request.params.uri, text: content }] };
});---
Index Once at Startup and Serve from Cache
**Detection**:
grep -rn 'indexDocs\|parseDoc\|WalkDir' --include="*.ts" src/ # Flag if found inside a setRequestHandler callback
**Signal**:
server.setRequestHandler(ListResourcesRequestSchema, async () => {
// Re-parses all markdown files on every list call — seconds of delay
const docs = await this.indexDocs();
return { resources: docs.map((d) => ({ uri: d.uri, name: d.metadata.title })) };
});**Why this matters**: Claude calls `resources/list` repeatedly during a session. Re-parsing 1000 files takes 3-10 seconds each time. The LLM context window fills with timeout errors before any content arrives.
**Preferred action**: Index once at startup, serve from in-memory Map, use mtime-based cache invalidation:
private docsIndex = new Map<string, ParsedDoc>();
// Called once at startup
async indexDocs(): Promise<void> {
const files = await glob('**/*.md', { cwd: this.docsPath });Essays and writing behind this toolkit live at vexjoy.com. AI agents skip steps. "Looks correct" replaces running tests. "Trivial change" replaces verification.
Repo: notque/vexjoy-agent
Other agents on vexjoy-agent.
- ansible-automation-engineer
Ansible automation: playbooks, roles, collections, Molecule testing, Vault security.
Open agent - modules
**Scope**: Module selection patterns, builtin vs command/shell decisions, collection modules, and version-specific module changes **Version range**: ansible-core 2.14+ / Ansible Collections (community.general 7.0+) **Generated**: 2026-04-04 — verify against current Ansible
Open agent - testing
**Scope**: Molecule test scenarios, ansible-lint rules, idempotency validation, and check-mode patterns **Version range**: Molecule 6.0+ / ansible-lint 6.0+ / ansible-core 2.14+ **Generated**: 2026-04-04 — verify against current Molecule and ansible-lint documentation
Open agent - base-instructions
Universal operational rules injected by /do at agent dispatch. Domain-specific rules live in each agent's .md file.
Open agent - communication-patterns
**Scope**: Failure modes in agent output style — over-reporting, self-congratulation, verbose narration, and hedging. Covers what to detect and how to fix each. **Version range**: all versions **Generated**: 2026-05-11
Open agent - combat-effects-upgrade
Zero-dependency combat visual upgrades: CSS particle replacement, Framer Motion combat juice, CSS 3D card transforms.
Open agent

