@cyanheads/mcp-ts-core 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/README.md +1 -1
- package/dist/cli/init.js +41 -18
- package/dist/cli/init.js.map +1 -1
- package/dist/core/app.d.ts.map +1 -1
- package/dist/core/app.js +16 -3
- package/dist/core/app.js.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.d.ts.map +1 -1
- package/dist/mcp-server/transports/http/httpTransport.js +4 -2
- package/dist/mcp-server/transports/http/httpTransport.js.map +1 -1
- package/dist/storage/core/storageFactory.d.ts +1 -1
- package/dist/storage/core/storageFactory.d.ts.map +1 -1
- package/dist/storage/core/storageFactory.js +2 -2
- package/dist/storage/core/storageFactory.js.map +1 -1
- package/dist/utils/telemetry/instrumentation.d.ts.map +1 -1
- package/dist/utils/telemetry/instrumentation.js +3 -1
- package/dist/utils/telemetry/instrumentation.js.map +1 -1
- package/package.json +6 -9
- package/scripts/build.ts +129 -0
- package/scripts/clean.ts +90 -0
- package/scripts/devcheck.ts +962 -0
- package/scripts/tree.ts +324 -0
- package/skills/design-mcp-server/SKILL.md +191 -0
- package/skills/setup/SKILL.md +11 -6
- package/templates/AGENTS.md +26 -8
- package/templates/CLAUDE.md +26 -8
- package/templates/_tsconfig.json +2 -22
- package/templates/biome.template.json +1 -41
- package/templates/package.json +33 -11
- package/templates/vitest.config.ts +13 -11
package/scripts/tree.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Generates a visual tree representation of the project's directory structure.
|
|
3
|
+
* @module scripts/tree
|
|
4
|
+
* Respects .gitignore patterns and common exclusions (e.g., node_modules).
|
|
5
|
+
* Saves the tree to a markdown file (default: docs/tree.md).
|
|
6
|
+
* Supports custom output path, depth limitation, and additional ignore patterns.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* // Generate tree with default settings:
|
|
10
|
+
* // bun run tree
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Specify custom output path and depth:
|
|
14
|
+
* // bun run scripts/tree.ts ./documentation/structure.md --depth=3
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Add additional ignore patterns:
|
|
18
|
+
* // bun run scripts/tree.ts --ignore=coverage --ignore="*.log"
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Preview tree without writing to disk:
|
|
22
|
+
* // bun run scripts/tree.ts --dry-run
|
|
23
|
+
*/
|
|
24
|
+
import type { Dirent } from 'node:fs';
|
|
25
|
+
import { mkdir, readdir, readFile, realpath, writeFile } from 'node:fs/promises';
|
|
26
|
+
import { basename, dirname, join, posix, relative, resolve, sep } from 'node:path';
|
|
27
|
+
import ignore from 'ignore';
|
|
28
|
+
|
|
29
|
+
type Ignore = ReturnType<typeof ignore>;
|
|
30
|
+
|
|
31
|
+
const KNOWN_FLAGS = ['--depth', '--ignore', '--help', '--dry-run'] as const;
|
|
32
|
+
|
|
33
|
+
const DEFAULT_IGNORE_PATTERNS: string[] = [
|
|
34
|
+
'.git',
|
|
35
|
+
'node_modules',
|
|
36
|
+
'.DS_Store',
|
|
37
|
+
'dist',
|
|
38
|
+
'build',
|
|
39
|
+
'coverage',
|
|
40
|
+
'logs',
|
|
41
|
+
'.husky/_',
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
interface ParsedArgs {
|
|
45
|
+
dryRun: boolean;
|
|
46
|
+
extraIgnorePatterns: string[];
|
|
47
|
+
maxDepth: number;
|
|
48
|
+
outputPath: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
52
|
+
const result: ParsedArgs = {
|
|
53
|
+
outputPath: 'docs/tree.md',
|
|
54
|
+
maxDepth: Infinity,
|
|
55
|
+
extraIgnorePatterns: [],
|
|
56
|
+
dryRun: false,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
for (const arg of argv) {
|
|
60
|
+
if (arg === '--dry-run') {
|
|
61
|
+
result.dryRun = true;
|
|
62
|
+
} else if (arg.startsWith('--depth=')) {
|
|
63
|
+
const depthValue = parseInt(arg.split('=')[1] ?? '', 10);
|
|
64
|
+
if (!Number.isNaN(depthValue) && depthValue >= 0) {
|
|
65
|
+
result.maxDepth = depthValue;
|
|
66
|
+
} else {
|
|
67
|
+
console.warn(`Invalid depth value: "${arg}". Using unlimited depth.`);
|
|
68
|
+
}
|
|
69
|
+
} else if (arg.startsWith('--ignore=')) {
|
|
70
|
+
const pattern = arg.slice('--ignore='.length);
|
|
71
|
+
if (pattern) {
|
|
72
|
+
result.extraIgnorePatterns.push(pattern);
|
|
73
|
+
}
|
|
74
|
+
} else if (arg.startsWith('--')) {
|
|
75
|
+
const flagName = arg.includes('=') ? arg.slice(0, arg.indexOf('=')) : arg;
|
|
76
|
+
if (!KNOWN_FLAGS.some((known) => flagName === known)) {
|
|
77
|
+
console.warn(`Unknown flag: "${arg}". Ignoring.`);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
result.outputPath = arg;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function validateOutputPath(outputPath: string, root: string): string {
|
|
88
|
+
const resolved = resolve(root, outputPath);
|
|
89
|
+
if (!resolved.startsWith(root + sep)) {
|
|
90
|
+
throw new Error(`Output path "${outputPath}" resolves outside project root: ${resolved}`);
|
|
91
|
+
}
|
|
92
|
+
const resolvedDir = dirname(resolved);
|
|
93
|
+
if (resolvedDir !== root && !resolvedDir.startsWith(root + sep)) {
|
|
94
|
+
throw new Error(`Output directory "${resolvedDir}" is outside project root`);
|
|
95
|
+
}
|
|
96
|
+
return resolved;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function loadIgnoreHandler(
|
|
100
|
+
root: string,
|
|
101
|
+
extraPatterns: string[],
|
|
102
|
+
outputFile: string,
|
|
103
|
+
): Promise<Ignore> {
|
|
104
|
+
const ig = ignore();
|
|
105
|
+
ig.add(DEFAULT_IGNORE_PATTERNS);
|
|
106
|
+
|
|
107
|
+
if (extraPatterns.length > 0) {
|
|
108
|
+
ig.add(extraPatterns);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Auto-ignore the output file so the generated artifact doesn't list itself
|
|
112
|
+
const outputRelative = relative(root, outputFile).split(sep).join(posix.sep);
|
|
113
|
+
ig.add(outputRelative);
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
const gitignoreContent = await readFile(join(root, '.gitignore'), 'utf-8');
|
|
117
|
+
ig.add(gitignoreContent);
|
|
118
|
+
} catch (error: unknown) {
|
|
119
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
120
|
+
console.warn(
|
|
121
|
+
'Info: No .gitignore file found at project root. Using default ignore patterns only.',
|
|
122
|
+
);
|
|
123
|
+
} else {
|
|
124
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
125
|
+
console.error(`Error reading .gitignore: ${msg}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return ig;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isIgnored(entryPath: string, root: string, ig: Ignore): boolean {
|
|
132
|
+
const rel = relative(root, entryPath).split(sep).join(posix.sep);
|
|
133
|
+
return ig.ignores(rel);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Recursively generates a string representation of the directory tree.
|
|
138
|
+
* Uses sequential traversal to avoid unbounded file-descriptor pressure.
|
|
139
|
+
* Tracks visited real paths to prevent symlink cycles.
|
|
140
|
+
*/
|
|
141
|
+
async function generateTree(
|
|
142
|
+
dir: string,
|
|
143
|
+
root: string,
|
|
144
|
+
ig: Ignore,
|
|
145
|
+
maxDepth: number,
|
|
146
|
+
prefix = '',
|
|
147
|
+
currentDepth = 0,
|
|
148
|
+
visited = new Set<string>(),
|
|
149
|
+
): Promise<string> {
|
|
150
|
+
const resolvedDir = resolve(dir);
|
|
151
|
+
if (!resolvedDir.startsWith(root + sep) && resolvedDir !== root) {
|
|
152
|
+
console.warn(`Security: Skipping directory outside project root: ${resolvedDir}`);
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (currentDepth > maxDepth) {
|
|
157
|
+
return '';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Resolve symlinks and detect cycles
|
|
161
|
+
let realDir: string;
|
|
162
|
+
try {
|
|
163
|
+
realDir = await realpath(resolvedDir);
|
|
164
|
+
} catch {
|
|
165
|
+
return '';
|
|
166
|
+
}
|
|
167
|
+
if (visited.has(realDir)) {
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
visited.add(realDir);
|
|
171
|
+
|
|
172
|
+
let entries: Dirent[];
|
|
173
|
+
try {
|
|
174
|
+
entries = await readdir(resolvedDir, { withFileTypes: true });
|
|
175
|
+
} catch (error: unknown) {
|
|
176
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
177
|
+
console.error(`Error reading directory ${resolvedDir}: ${msg}`);
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const filteredEntries = entries
|
|
182
|
+
.filter((entry) => !isIgnored(join(resolvedDir, entry.name), root, ig))
|
|
183
|
+
.sort((a, b) => {
|
|
184
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
185
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
186
|
+
return a.name.localeCompare(b.name);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Sequential traversal — prevents unbounded concurrent readdir calls
|
|
190
|
+
let result = '';
|
|
191
|
+
for (let i = 0; i < filteredEntries.length; i++) {
|
|
192
|
+
const entry = filteredEntries[i];
|
|
193
|
+
const isLast = i === filteredEntries.length - 1;
|
|
194
|
+
const connector = isLast ? '\u2514\u2500\u2500 ' : '\u251C\u2500\u2500 ';
|
|
195
|
+
const newPrefix = prefix + (isLast ? ' ' : '\u2502 ');
|
|
196
|
+
const displayName = entry.isDirectory() ? `${entry.name}/` : entry.name;
|
|
197
|
+
|
|
198
|
+
result += `${prefix + connector + displayName}\n`;
|
|
199
|
+
|
|
200
|
+
if (entry.isDirectory()) {
|
|
201
|
+
result += await generateTree(
|
|
202
|
+
join(resolvedDir, entry.name),
|
|
203
|
+
root,
|
|
204
|
+
ig,
|
|
205
|
+
maxDepth,
|
|
206
|
+
newPrefix,
|
|
207
|
+
currentDepth + 1,
|
|
208
|
+
visited,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Extracts the raw tree body from an existing output file for diffing.
|
|
218
|
+
* Returns null if the file doesn't exist or the tree block can't be parsed.
|
|
219
|
+
*/
|
|
220
|
+
async function readExistingTree(outputFile: string, projectName: string): Promise<string | null> {
|
|
221
|
+
let content: string;
|
|
222
|
+
try {
|
|
223
|
+
content = await readFile(outputFile, 'utf-8');
|
|
224
|
+
} catch (error: unknown) {
|
|
225
|
+
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return null;
|
|
226
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
227
|
+
console.warn(`Warning: Could not read existing output file for comparison: ${msg}`);
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const escaped = projectName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
232
|
+
const regex = new RegExp(
|
|
233
|
+
`^\\s*\`\`\`(?:[^\\n]*)\\n${escaped}/?\\n([\\s\\S]*?)\\n\`\`\`\\s*$`,
|
|
234
|
+
'm',
|
|
235
|
+
);
|
|
236
|
+
const match = content.match(regex);
|
|
237
|
+
return match && typeof match[1] === 'string' ? match[1] : null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function buildOutputContent(projectName: string, treeContent: string, maxDepth: number): string {
|
|
241
|
+
const timestamp = new Date().toISOString().replace(/T/, ' ').replace(/\..+/, '');
|
|
242
|
+
const header = `# ${projectName} - Directory Structure\n\nGenerated on: ${timestamp}\n`;
|
|
243
|
+
const depthInfo = maxDepth !== Infinity ? `\n_Depth limited to ${maxDepth} levels_\n\n` : '\n';
|
|
244
|
+
const treeBlock = `\`\`\`\n${projectName}/\n${treeContent}\`\`\`\n`;
|
|
245
|
+
const footer = `\n_Note: This tree excludes files and directories matched by .gitignore and default patterns._\n`;
|
|
246
|
+
return header + depthInfo + treeBlock + footer;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const normalize = (str: string | null) => str?.replace(/\r\n/g, '\n').trimEnd() ?? null;
|
|
250
|
+
|
|
251
|
+
const generateDirectoryTree = async (): Promise<void> => {
|
|
252
|
+
try {
|
|
253
|
+
const root = process.cwd();
|
|
254
|
+
const args = process.argv.slice(2);
|
|
255
|
+
|
|
256
|
+
if (args.includes('--help')) {
|
|
257
|
+
console.log(`
|
|
258
|
+
Generate Tree - Project directory structure visualization tool
|
|
259
|
+
|
|
260
|
+
Usage:
|
|
261
|
+
bun run scripts/tree.ts [output-path] [options]
|
|
262
|
+
|
|
263
|
+
Options:
|
|
264
|
+
output-path Custom file path for the tree output (relative to project root, default: docs/tree.md)
|
|
265
|
+
--depth=<number> Maximum directory depth to display (default: unlimited)
|
|
266
|
+
--ignore=<pattern> Additional ignore pattern (can be specified multiple times)
|
|
267
|
+
--dry-run Print tree to stdout without writing to disk
|
|
268
|
+
--help Show this help message
|
|
269
|
+
`);
|
|
270
|
+
process.exit(0);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const parsed = parseArgs(args);
|
|
274
|
+
const projectName = basename(root);
|
|
275
|
+
const resolvedOutputFile = validateOutputPath(parsed.outputPath, root);
|
|
276
|
+
const ignoreHandler = await loadIgnoreHandler(
|
|
277
|
+
root,
|
|
278
|
+
parsed.extraIgnorePatterns,
|
|
279
|
+
resolvedOutputFile,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
console.log(`Generating directory tree for project: ${projectName}`);
|
|
283
|
+
if (!parsed.dryRun) {
|
|
284
|
+
console.log(`Output will be saved to: ${resolvedOutputFile}`);
|
|
285
|
+
}
|
|
286
|
+
if (parsed.maxDepth !== Infinity) {
|
|
287
|
+
console.log(`Maximum depth set to: ${parsed.maxDepth}`);
|
|
288
|
+
}
|
|
289
|
+
if (parsed.extraIgnorePatterns.length > 0) {
|
|
290
|
+
console.log(`Additional ignore patterns: ${parsed.extraIgnorePatterns.join(', ')}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const treeContent = await generateTree(root, root, ignoreHandler, parsed.maxDepth);
|
|
294
|
+
|
|
295
|
+
if (parsed.dryRun) {
|
|
296
|
+
console.log(`\n${projectName}/`);
|
|
297
|
+
process.stdout.write(treeContent);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const existingTree = await readExistingTree(resolvedOutputFile, projectName);
|
|
302
|
+
|
|
303
|
+
if (normalize(existingTree) === normalize(treeContent)) {
|
|
304
|
+
console.log(
|
|
305
|
+
`Directory structure is unchanged. Output file not updated: ${resolvedOutputFile}`,
|
|
306
|
+
);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await mkdir(dirname(resolvedOutputFile), { recursive: true });
|
|
311
|
+
await writeFile(
|
|
312
|
+
resolvedOutputFile,
|
|
313
|
+
buildOutputContent(projectName, treeContent, parsed.maxDepth),
|
|
314
|
+
);
|
|
315
|
+
console.log(`Successfully generated and updated tree structure in: ${resolvedOutputFile}`);
|
|
316
|
+
} catch (error: unknown) {
|
|
317
|
+
console.error(
|
|
318
|
+
`Error generating tree: ${error instanceof Error ? error.message : String(error)}`,
|
|
319
|
+
);
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
void generateDirectoryTree();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: design-mcp-server
|
|
3
|
+
description: >
|
|
4
|
+
Design the tool surface, resources, and service layer for a new MCP server. Use when starting a new server, planning a major feature expansion, or when the user describes a domain/API they want to expose via MCP. Produces a design doc at docs/design.md that drives implementation.
|
|
5
|
+
metadata:
|
|
6
|
+
author: cyanheads
|
|
7
|
+
version: "1.1"
|
|
8
|
+
audience: external
|
|
9
|
+
type: workflow
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## When to Use
|
|
13
|
+
|
|
14
|
+
- User says "I want to build a ___ MCP server"
|
|
15
|
+
- User has an API, database, or system they want to expose to LLMs
|
|
16
|
+
- User wants to plan tools before scaffolding
|
|
17
|
+
- Existing server needs a new capability area (design the addition, not just a single tool)
|
|
18
|
+
|
|
19
|
+
Do NOT use for single-tool additions — use `add-tool` directly.
|
|
20
|
+
|
|
21
|
+
## Inputs
|
|
22
|
+
|
|
23
|
+
Gather before designing. Ask the user if not obvious from context:
|
|
24
|
+
|
|
25
|
+
1. **Domain** — what system, API, or capability is this server wrapping?
|
|
26
|
+
2. **Data sources** — APIs, databases, file systems, external services?
|
|
27
|
+
3. **Target users** — what will the LLM (and its human) be trying to accomplish?
|
|
28
|
+
4. **Scope constraints** — read-only? write access? admin operations? what's off-limits?
|
|
29
|
+
|
|
30
|
+
If the domain has a public API, read its docs before designing. Don't design from vibes.
|
|
31
|
+
|
|
32
|
+
## Steps
|
|
33
|
+
|
|
34
|
+
### 1. Research External Dependencies
|
|
35
|
+
|
|
36
|
+
Before designing, verify the APIs and services the server will wrap.
|
|
37
|
+
|
|
38
|
+
If the Agent tool is available, spawn background agents to research in parallel while you proceed with domain mapping:
|
|
39
|
+
|
|
40
|
+
- Fetch API docs, confirm endpoint availability, auth methods, rate limits
|
|
41
|
+
- Check for official SDKs or client libraries (npm packages)
|
|
42
|
+
- Note any API quirks, pagination patterns, or data format considerations
|
|
43
|
+
|
|
44
|
+
If the Agent tool is not available, do this research inline — fetch docs, read SDK readmes, confirm assumptions before committing them to the design.
|
|
45
|
+
|
|
46
|
+
### 2. Map the Domain
|
|
47
|
+
|
|
48
|
+
List the concrete operations the underlying system supports. Group by domain noun.
|
|
49
|
+
|
|
50
|
+
Example for a project management API:
|
|
51
|
+
|
|
52
|
+
| Noun | Operations |
|
|
53
|
+
|:-----|:-----------|
|
|
54
|
+
| Project | list, get, create, archive |
|
|
55
|
+
| Task | list (by project), get, create, update status, assign, comment |
|
|
56
|
+
| User | list, get current |
|
|
57
|
+
|
|
58
|
+
This is the raw material. Not everything becomes a tool.
|
|
59
|
+
|
|
60
|
+
### 3. Classify into MCP Primitives
|
|
61
|
+
|
|
62
|
+
| Primitive | Use when | Examples |
|
|
63
|
+
|:----------|:---------|:--------|
|
|
64
|
+
| **Tool** | Needs parameters beyond a simple ID, has side effects, or requires LLM decisions about inputs | Search, create, update, analyze, transform |
|
|
65
|
+
| **Resource** | Addressable by stable URI, read-only, useful as injectable context | Config, schemas, status, entity-by-ID lookups |
|
|
66
|
+
| **Prompt** | Reusable message template that structures how the LLM approaches a task | Analysis framework, report template, review checklist |
|
|
67
|
+
| **Neither** | Internal detail, admin-only, not useful to an LLM | Token refresh, webhook setup, migrations |
|
|
68
|
+
|
|
69
|
+
**Common traps:**
|
|
70
|
+
|
|
71
|
+
- **Everything-is-a-tool**: "Fetch by ID" with no other params is a resource. Resources let clients inject context without a tool call.
|
|
72
|
+
- **CRUD explosion**: Don't map every REST endpoint to a tool. One `update_task` beats `update_task_status` + `assign_task` + `update_task_fields`.
|
|
73
|
+
- **Ignoring resources**: If the server has reference data, schemas, or entities the LLM should read — expose them as resources.
|
|
74
|
+
|
|
75
|
+
### 4. Design Tools
|
|
76
|
+
|
|
77
|
+
For each tool:
|
|
78
|
+
|
|
79
|
+
| Aspect | Decision |
|
|
80
|
+
|:-------|:---------|
|
|
81
|
+
| **Name** | `snake_case`, verb-noun: `search_papers`, `create_task`. Prefix with server domain if ambiguous. |
|
|
82
|
+
| **Granularity** | One tool per user intent, not per API call. A tool can call multiple APIs internally. |
|
|
83
|
+
| **Input schema** | What the LLM provides. `.describe()` on every field. Prefer enums/literals over free strings. Optional fields with defaults over required where reasonable. |
|
|
84
|
+
| **Output schema** | What the LLM needs to see. Curate — not a raw API dump. Design for the LLM's next decision, not for a UI. |
|
|
85
|
+
| **Annotations** | `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`. Helps clients auto-approve safely. |
|
|
86
|
+
| **Auth scopes** | `tool:noun:read`, `tool:noun:write`. Skip for read-only or stdio-only servers. |
|
|
87
|
+
|
|
88
|
+
### 5. Design Resources
|
|
89
|
+
|
|
90
|
+
For each resource:
|
|
91
|
+
|
|
92
|
+
| Aspect | Decision |
|
|
93
|
+
|:-------|:---------|
|
|
94
|
+
| **URI template** | `scheme://{param}/path`. Server domain as scheme. Keep shallow. |
|
|
95
|
+
| **Params** | Minimal — typically just an identifier. Complex queries belong in tools. |
|
|
96
|
+
| **Pagination** | Needed if lists exceed ~50 items. Opaque cursors via `extractCursor`/`paginateArray`. |
|
|
97
|
+
| **list()** | Provide if discoverable. Top-level categories or recent items, not exhaustive dumps. |
|
|
98
|
+
|
|
99
|
+
### 6. Design Prompts (if needed)
|
|
100
|
+
|
|
101
|
+
Optional. Use when the server has recurring interaction patterns worth structuring:
|
|
102
|
+
- Analysis frameworks, report templates, multi-step workflows
|
|
103
|
+
|
|
104
|
+
Skip for purely data/action-oriented servers.
|
|
105
|
+
|
|
106
|
+
### 7. Plan Services and Config
|
|
107
|
+
|
|
108
|
+
**Services** — one per external dependency. Init/accessor pattern. Skip if all tools are thin wrappers with no shared state.
|
|
109
|
+
|
|
110
|
+
**Config** — list env vars (API keys, base URLs). Goes in `src/config/server-config.ts` as a separate Zod schema.
|
|
111
|
+
|
|
112
|
+
### 8. Write the Design Doc
|
|
113
|
+
|
|
114
|
+
Create `docs/design.md` with the structure below. The MCP surface (tools, resources, prompts) goes first — it's what matters most and what the developer will reference during implementation.
|
|
115
|
+
|
|
116
|
+
```markdown
|
|
117
|
+
# {{Server Name}} — Design
|
|
118
|
+
|
|
119
|
+
## MCP Surface
|
|
120
|
+
|
|
121
|
+
### Tools
|
|
122
|
+
| Name | Description | Key Inputs | Annotations |
|
|
123
|
+
|:-----|:------------|:-----------|:------------|
|
|
124
|
+
|
|
125
|
+
### Resources
|
|
126
|
+
| URI Template | Description | Pagination |
|
|
127
|
+
|:-------------|:------------|:-----------|
|
|
128
|
+
|
|
129
|
+
### Prompts
|
|
130
|
+
| Name | Description | Args |
|
|
131
|
+
|:-----|:------------|:-----|
|
|
132
|
+
|
|
133
|
+
## Overview
|
|
134
|
+
|
|
135
|
+
What this server does, what system it wraps, who it's for.
|
|
136
|
+
|
|
137
|
+
## Requirements
|
|
138
|
+
|
|
139
|
+
- Bullet list of capabilities and constraints
|
|
140
|
+
- Auth requirements, rate limits, data access scope
|
|
141
|
+
|
|
142
|
+
## Services
|
|
143
|
+
| Service | Wraps | Used By |
|
|
144
|
+
|:--------|:------|:--------|
|
|
145
|
+
|
|
146
|
+
## Config
|
|
147
|
+
| Env Var | Required | Description |
|
|
148
|
+
|:--------|:---------|:------------|
|
|
149
|
+
|
|
150
|
+
## Implementation Order
|
|
151
|
+
|
|
152
|
+
1. Config and server setup
|
|
153
|
+
2. Services (external API clients)
|
|
154
|
+
3. Read-only tools
|
|
155
|
+
4. Write tools
|
|
156
|
+
5. Resources
|
|
157
|
+
6. Prompts
|
|
158
|
+
|
|
159
|
+
Each step is independently testable.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Keep it concise. The design doc is a working reference, not a spec document — enough to orient a developer (or agent) implementing the server, not more.
|
|
163
|
+
|
|
164
|
+
### 9. Confirm and Proceed
|
|
165
|
+
|
|
166
|
+
If the user has already authorized implementation (e.g., "build me a ___ server"), proceed directly to scaffolding using the design doc as the plan. Otherwise, present the design doc to the user for review before implementing.
|
|
167
|
+
|
|
168
|
+
## After Design
|
|
169
|
+
|
|
170
|
+
Execute the plan using the scaffolding skills:
|
|
171
|
+
|
|
172
|
+
1. `add-service` for each service
|
|
173
|
+
2. `add-tool` for each tool
|
|
174
|
+
3. `add-resource` for each resource
|
|
175
|
+
4. `add-prompt` for each prompt
|
|
176
|
+
5. `devcheck` after each addition
|
|
177
|
+
|
|
178
|
+
## Checklist
|
|
179
|
+
|
|
180
|
+
- [ ] External APIs/dependencies researched and verified (docs fetched, SDKs identified)
|
|
181
|
+
- [ ] Domain operations mapped (nouns + verbs)
|
|
182
|
+
- [ ] Each operation classified as tool, resource, prompt, or excluded
|
|
183
|
+
- [ ] Tool names follow verb-noun `snake_case` convention
|
|
184
|
+
- [ ] Tool outputs designed for LLM consumption, not raw API passthrough
|
|
185
|
+
- [ ] Annotations set correctly (`readOnlyHint`, `destructiveHint`, etc.)
|
|
186
|
+
- [ ] Input schemas use constrained types (enums, literals) where the domain allows
|
|
187
|
+
- [ ] Resource URIs use `{param}` templates, pagination planned for large lists
|
|
188
|
+
- [ ] Service layer planned (or explicitly skipped with reasoning)
|
|
189
|
+
- [ ] Server config env vars identified
|
|
190
|
+
- [ ] Design doc written to `docs/design.md`
|
|
191
|
+
- [ ] Design confirmed with user (or user pre-authorized implementation)
|
package/skills/setup/SKILL.md
CHANGED
|
@@ -80,13 +80,17 @@ The init creates echo definitions for tools, resources, and prompts. They're fun
|
|
|
80
80
|
|
|
81
81
|
## Skill Sync
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
Copy all project skills into your agent's skill directory so they're available as context. `skills/` is the source of truth.
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
2. Copy any missing or updated skills into your agent directory
|
|
87
|
-
3. Do not remove server-specific skills from either location
|
|
85
|
+
**For Claude Code:**
|
|
88
86
|
|
|
89
|
-
|
|
87
|
+
```bash
|
|
88
|
+
mkdir -p .claude/skills && cp -R skills/* .claude/skills/
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**For other agents** (Codex, Cursor, Windsurf, etc.) — copy to the equivalent directory (e.g., `.codex/skills/`, `.cursor/skills/`).
|
|
92
|
+
|
|
93
|
+
After the initial copy, use the `maintenance` skill to keep them in sync after package updates.
|
|
90
94
|
|
|
91
95
|
## Project Scaffolding
|
|
92
96
|
|
|
@@ -101,6 +105,7 @@ After `bun install`, complete these one-time setup tasks:
|
|
|
101
105
|
- [ ] `{{PACKAGE_NAME}}` placeholders replaced in agent protocol file (if not auto-substituted by init)
|
|
102
106
|
- [ ] Core framework CLAUDE.md read (`node_modules/@cyanheads/mcp-ts-core/CLAUDE.md`)
|
|
103
107
|
- [ ] Unused echo definitions deleted (and unregistered from `src/index.ts`)
|
|
104
|
-
- [ ]
|
|
108
|
+
- [ ] Skills copied to agent directory (`cp -R skills/* .claude/skills/` or equivalent)
|
|
105
109
|
- [ ] Project structure understood (definitions directories, entry point)
|
|
106
110
|
- [ ] `bun run devcheck` passes
|
|
111
|
+
- [ ] If new server: proceed to `design-mcp-server` skill to plan the tool surface
|
package/templates/AGENTS.md
CHANGED
|
@@ -1,13 +1,23 @@
|
|
|
1
1
|
# Agent Protocol
|
|
2
2
|
|
|
3
3
|
**Server:** {{PACKAGE_NAME}}
|
|
4
|
-
**Version:** 0.1.
|
|
4
|
+
**Version:** 0.1.2
|
|
5
5
|
**Framework:** [@cyanheads/mcp-ts-core](https://www.npmjs.com/package/@cyanheads/mcp-ts-core)
|
|
6
6
|
|
|
7
7
|
> **Read the framework docs first:** `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md` contains the full API reference — builders, Context, error codes, exports, patterns. This file covers server-specific conventions only.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
11
|
+
## First Session
|
|
12
|
+
|
|
13
|
+
> **Remove this section** from CLAUDE.md / AGENTS.md after completing these steps. The skills and conventions below remain — this block is one-time onboarding only.
|
|
14
|
+
|
|
15
|
+
1. **Read the framework API** — `node_modules/@cyanheads/mcp-ts-core/CLAUDE.md`
|
|
16
|
+
2. **Run the `setup` skill** — read `skills/setup/SKILL.md` and follow its checklist (project orientation, agent protocol file selection, echo definition cleanup, skill sync)
|
|
17
|
+
3. **Design the server** — read `skills/design-mcp-server/SKILL.md` and work through it with the user to map the domain into tools, resources, and services before scaffolding
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
11
21
|
## Core Rules
|
|
12
22
|
|
|
13
23
|
- **Logic throws, framework catches.** Tool/resource handlers are pure — throw on failure, no `try/catch`. Plain `Error` is fine; the framework catches, classifies, and formats. Use error factories (`notFound()`, `validationError()`, etc.) when the error code matters.
|
|
@@ -173,6 +183,7 @@ src/
|
|
|
173
183
|
| Files | kebab-case with suffix | `search-docs.tool.ts` |
|
|
174
184
|
| Tool/resource/prompt names | snake_case | `search_docs` |
|
|
175
185
|
| Directories | kebab-case | `src/services/doc-search/` |
|
|
186
|
+
| Descriptions | Single string or template literal, no `+` concatenation | `'Search items by query and filter.'` |
|
|
176
187
|
|
|
177
188
|
---
|
|
178
189
|
|
|
@@ -187,6 +198,7 @@ Available skills:
|
|
|
187
198
|
| Skill | Purpose |
|
|
188
199
|
|:------|:--------|
|
|
189
200
|
| `setup` | Post-init project orientation |
|
|
201
|
+
| `design-mcp-server` | Design tool surface, resources, and services for a new server |
|
|
190
202
|
| `add-tool` | Scaffold a new tool definition |
|
|
191
203
|
| `add-resource` | Scaffold a new resource definition |
|
|
192
204
|
| `add-prompt` | Scaffold a new prompt definition |
|
|
@@ -211,12 +223,18 @@ When you complete a skill's checklist, check the boxes and add a completion time
|
|
|
211
223
|
|
|
212
224
|
| Command | Purpose |
|
|
213
225
|
|:--------|:--------|
|
|
214
|
-
| `
|
|
215
|
-
| `
|
|
216
|
-
| `bun run
|
|
217
|
-
| `bun run
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
226
|
+
| `npm run build` | Compile TypeScript |
|
|
227
|
+
| `npm run clean` | Remove build artifacts |
|
|
228
|
+
| `bun run devcheck` | Lint + format + typecheck + security |
|
|
229
|
+
| `bun run tree` | Generate directory structure doc |
|
|
230
|
+
| `npm run format` | Auto-fix formatting |
|
|
231
|
+
| `npm test` | Run tests |
|
|
232
|
+
| `npm run dev:stdio` | Dev mode (stdio) |
|
|
233
|
+
| `npm run dev:http` | Dev mode (HTTP) |
|
|
234
|
+
| `npm run start:stdio` | Production mode (stdio) |
|
|
235
|
+
| `npm run start:http` | Production mode (HTTP) |
|
|
236
|
+
|
|
237
|
+
**Bun requirement:** `devcheck` and `tree` scripts use Bun-specific APIs (`spawn` from `'bun'`). Install [Bun](https://bun.sh) to run them. All other commands work with any Node-compatible package manager.
|
|
220
238
|
|
|
221
239
|
---
|
|
222
240
|
|
|
@@ -239,6 +257,6 @@ import { getMyService } from '@/services/my-domain/my-service.js';
|
|
|
239
257
|
- [ ] JSDoc `@fileoverview` + `@module` on every file
|
|
240
258
|
- [ ] `ctx.log` for logging, `ctx.state` for storage
|
|
241
259
|
- [ ] Handlers throw on failure — error factories or plain `Error`, no try/catch
|
|
242
|
-
- [ ] Registered in `
|
|
260
|
+
- [ ] Registered in `createApp()` arrays (directly or via barrel exports)
|
|
243
261
|
- [ ] Tests use `createMockContext()` from `@cyanheads/mcp-ts-core/testing`
|
|
244
262
|
- [ ] `bun run devcheck` passes
|