mcpill 1.1.0

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.
Files changed (55) hide show
  1. package/.claude/commands/add-card.md +9 -0
  2. package/.claude/commands/append-card.md +10 -0
  3. package/.claude/commands/play-card.md +14 -0
  4. package/.claude/commands/turn.md +28 -0
  5. package/.claude/settings.json +10 -0
  6. package/.flowdeck/.flowdeckignore +5 -0
  7. package/.flowdeck/AGENT.md +46 -0
  8. package/.flowdeck/TODO.md.template +14 -0
  9. package/.flowdeck/_discard/start/DISCARD.md +8 -0
  10. package/.flowdeck/_discard/start/TODO.md +35 -0
  11. package/.flowdeck/_discard/start/turn.log +28 -0
  12. package/.flowdeck/_energy/ADR.md.template +100 -0
  13. package/.flowdeck/_energy/CLAUDE.md.template +95 -0
  14. package/.flowdeck/_energy/GENERALINSIGHTS.md.template +57 -0
  15. package/.flowdeck/_energy/MISSION.md.template +89 -0
  16. package/.flowdeck/_energy/OPEN-QUESTIONS.md.template +109 -0
  17. package/.flowdeck/_energy/PROJECTINSIGHTS.md.template +64 -0
  18. package/.flowdeck/_energy/SPEC.md.template +101 -0
  19. package/.flowdeck/_frozen/FROZEN.md +4 -0
  20. package/.flowdeck/_meld/MELD.md +4 -0
  21. package/.flowdeck/_stock/STOCK.md +4 -0
  22. package/.flowdeck/reframe/TODO.md +71 -0
  23. package/CHANGELOG.md +8 -0
  24. package/FLOWDECK.md +17 -0
  25. package/README.md +85 -0
  26. package/dist/cli.js +954 -0
  27. package/package.json +34 -0
  28. package/src/__tests__/init.test.ts +74 -0
  29. package/src/__tests__/loaders/config.test.ts +54 -0
  30. package/src/__tests__/loaders/prompts.test.ts +116 -0
  31. package/src/__tests__/loaders/resources.test.ts +86 -0
  32. package/src/__tests__/loaders/tools.test.ts +128 -0
  33. package/src/__tests__/pack.test.ts +98 -0
  34. package/src/__tests__/validate.test.ts +152 -0
  35. package/src/cli.ts +76 -0
  36. package/src/commands/compile.ts +163 -0
  37. package/src/commands/init.ts +145 -0
  38. package/src/commands/pack.ts +52 -0
  39. package/src/commands/publish.ts +17 -0
  40. package/src/commands/run.ts +101 -0
  41. package/src/commands/validate.ts +70 -0
  42. package/src/compiler/merge-tools.ts +99 -0
  43. package/src/compiler/parse.ts +236 -0
  44. package/src/compiler/serialize.ts +100 -0
  45. package/src/compiler/types.ts +27 -0
  46. package/src/loaders/config.ts +25 -0
  47. package/src/loaders/prompts.ts +60 -0
  48. package/src/loaders/resources.ts +54 -0
  49. package/src/loaders/tools.ts +63 -0
  50. package/src/templates/prompts/greeting.md +11 -0
  51. package/src/templates/server.md +9 -0
  52. package/src/templates/tools/echo.md +15 -0
  53. package/tsconfig.json +10 -0
  54. package/tsup.config.ts +13 -0
  55. package/vitest.config.ts +12 -0
@@ -0,0 +1,101 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+ import { z } from "mcpill-runtime";
4
+ import { createServer } from "mcpster";
5
+ import { loadConfig } from "../loaders/config.js";
6
+ import { loadTools } from "../loaders/tools.js";
7
+ import { loadPrompts } from "../loaders/prompts.js";
8
+ import { loadResources } from "../loaders/resources.js";
9
+ import { runValidate } from "./validate.js";
10
+
11
+ type ZodRawShape = Record<string, z.ZodTypeAny>;
12
+
13
+ export async function runServer(opts: {
14
+ dir?: string;
15
+ transport?: "stdio" | "http";
16
+ port?: number;
17
+ }): Promise<void> {
18
+ const baseDir = path.resolve(opts.dir ?? process.cwd());
19
+
20
+ await runValidate(baseDir);
21
+
22
+ const pillDirs = fs
23
+ .readdirSync(baseDir, { withFileTypes: true })
24
+ .filter(
25
+ (e) =>
26
+ e.isDirectory() &&
27
+ /^\.[a-z][a-z0-9-]*$/.test(e.name) &&
28
+ fs.existsSync(path.join(baseDir, e.name, "mcpill.config.json")),
29
+ )
30
+ .map((e) => path.join(baseDir, e.name));
31
+
32
+ const mcpillDir = pillDirs[0]!;
33
+
34
+ const [tools, prompts, resources, config] = await Promise.all([
35
+ loadTools(mcpillDir),
36
+ loadPrompts(mcpillDir),
37
+ loadResources(mcpillDir),
38
+ loadConfig(mcpillDir),
39
+ ]);
40
+
41
+ const transport = opts.transport ?? config.transport;
42
+ const port = opts.port ?? config.port;
43
+ const { name } = config;
44
+
45
+ const server = createServer({
46
+ name,
47
+ version: "0.1.0",
48
+ transport,
49
+ http: { port },
50
+ });
51
+
52
+ for (const { name: toolName, description, schema, handler } of tools) {
53
+ server.defineTool({
54
+ name: toolName,
55
+ description,
56
+ schema: z.object(schema as ZodRawShape),
57
+ handler: handler as (input: Record<string, unknown>) => Promise<unknown>,
58
+ });
59
+ }
60
+
61
+ for (const { name: promptName, description, messages } of prompts) {
62
+ server.definePrompt({
63
+ name: promptName,
64
+ description,
65
+ handler: async (args: Record<string, string>) =>
66
+ messages
67
+ .map((m) =>
68
+ m.content.replace(
69
+ /\{\{(\w+)\}\}/g,
70
+ (_, k: string) => String(args[k] ?? "")
71
+ )
72
+ )
73
+ .join("\n"),
74
+ });
75
+ }
76
+
77
+ for (const { uri, name: resourceName, content } of resources) {
78
+ server.defineResource({
79
+ uri,
80
+ description: resourceName ?? uri,
81
+ resolver: async () => content,
82
+ });
83
+ }
84
+
85
+ try {
86
+ await server.start();
87
+ } catch (err) {
88
+ const nodeErr = err as NodeJS.ErrnoException;
89
+ if (nodeErr.code === "EADDRINUSE") {
90
+ console.error(
91
+ `Port ${port} is already in use. Use --port to specify another.`
92
+ );
93
+ process.exit(1);
94
+ }
95
+ throw err;
96
+ }
97
+
98
+ process.stderr.write(
99
+ `✓ MCPill running — ${tools.length} tools, ${prompts.length} prompts, ${resources.length} resources [${transport}]\n`
100
+ );
101
+ }
@@ -0,0 +1,70 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { loadConfig } from "../loaders/config.js";
4
+ import { loadTools } from "../loaders/tools.js";
5
+ import { loadPrompts } from "../loaders/prompts.js";
6
+ import { loadResources } from "../loaders/resources.js";
7
+
8
+ async function validateOne(mcpillDir: string): Promise<void> {
9
+ const errors: string[] = [];
10
+ let toolCount = 0;
11
+ let promptCount = 0;
12
+ let resourceCount = 0;
13
+
14
+ try {
15
+ const tools = await loadTools(mcpillDir);
16
+ toolCount = tools.length;
17
+ } catch (err) {
18
+ errors.push(err instanceof Error ? err.message : String(err));
19
+ }
20
+
21
+ try {
22
+ const prompts = await loadPrompts(mcpillDir);
23
+ promptCount = prompts.length;
24
+ } catch (err) {
25
+ errors.push(err instanceof Error ? err.message : String(err));
26
+ }
27
+
28
+ try {
29
+ const resources = await loadResources(mcpillDir);
30
+ resourceCount = resources.length;
31
+ } catch (err) {
32
+ errors.push(err instanceof Error ? err.message : String(err));
33
+ }
34
+
35
+ try {
36
+ await loadConfig(mcpillDir);
37
+ } catch (err) {
38
+ errors.push(err instanceof Error ? err.message : String(err));
39
+ }
40
+
41
+ if (errors.length > 0) {
42
+ for (const error of errors) {
43
+ console.error(error);
44
+ }
45
+ process.exit(1);
46
+ }
47
+
48
+ console.log(`✓ Valid: ${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources`);
49
+ }
50
+
51
+ export async function runValidate(baseDir: string): Promise<void> {
52
+ const pillDirs = fs
53
+ .readdirSync(baseDir, { withFileTypes: true })
54
+ .filter(
55
+ (e) =>
56
+ e.isDirectory() &&
57
+ /^\.[a-z][a-z0-9-]*$/.test(e.name) &&
58
+ fs.existsSync(path.join(baseDir, e.name, "mcpill.config.json")),
59
+ )
60
+ .map((e) => path.join(baseDir, e.name));
61
+
62
+ if (pillDirs.length === 0) {
63
+ console.error("No pill directories found — run mcpill compile first");
64
+ process.exit(1);
65
+ }
66
+
67
+ for (const mcpillDir of pillDirs) {
68
+ await validateOne(mcpillDir);
69
+ }
70
+ }
@@ -0,0 +1,99 @@
1
+ import type { ServerDoc } from './types.js';
2
+
3
+ function dedent(s: string): string {
4
+ const lines = s.split('\n');
5
+ const nonEmpty = lines.filter((l) => l.trim());
6
+ if (nonEmpty.length === 0) return s;
7
+ const minIndent = nonEmpty.reduce((min, l) => {
8
+ const m = l.match(/^([ \t]*)/);
9
+ return Math.min(min, m?.[1]?.length ?? 0);
10
+ }, Infinity);
11
+ return lines.map((l) => l.slice(minIndent === Infinity ? 0 : minIndent)).join('\n');
12
+ }
13
+
14
+ export function extractHandlers(toolsJsContent: string): Map<string, string> {
15
+ const result = new Map<string, string>();
16
+
17
+ // Marker-based extraction: // @handler:name ... // @end-handler:name
18
+ // Allow leading whitespace before the // so indented markers in tools.js are matched.
19
+ const markerRegex = /[ \t]*\/\/ @handler:([^\n]+)\n([\s\S]*?)\n[ \t]*\/\/ @end-handler:[^\n]+/g;
20
+ let match: RegExpExecArray | null;
21
+ while ((match = markerRegex.exec(toolsJsContent)) !== null) {
22
+ result.set(match[1]!.trim(), dedent(match[2]!).trim());
23
+ }
24
+
25
+ if (result.size > 0) return result;
26
+
27
+ // Fallback: find name: "toolname" then locate handler: expression via brace counting
28
+ const nameRegex = /name:\s*["']([^"']+)["']/g;
29
+ while ((match = nameRegex.exec(toolsJsContent)) !== null) {
30
+ const toolName = match[1]!;
31
+ const handlerKeyIdx = toolsJsContent.indexOf('handler:', match.index);
32
+ if (handlerKeyIdx === -1) continue;
33
+ const expr = extractExpression(toolsJsContent, handlerKeyIdx + 'handler:'.length);
34
+ if (expr) result.set(toolName, expr);
35
+ }
36
+
37
+ return result;
38
+ }
39
+
40
+ function extractExpression(src: string, start: number): string | null {
41
+ let i = start;
42
+ while (i < src.length && /[ \t]/.test(src[i]!)) i++;
43
+ const exprStart = i;
44
+ let depth = 0;
45
+ let inStr: string | null = null;
46
+
47
+ while (i < src.length) {
48
+ const ch = src[i]!;
49
+ if (inStr) {
50
+ if (ch === '\\') { i += 2; continue; }
51
+ if (ch === inStr) inStr = null;
52
+ } else if (ch === '"' || ch === "'" || ch === '`') {
53
+ inStr = ch;
54
+ } else if (ch === '{' || ch === '(' || ch === '[') {
55
+ depth++;
56
+ } else if (ch === '}' || ch === ')' || ch === ']') {
57
+ depth--;
58
+ if (depth === 0) {
59
+ // Peek ahead: if followed by => this is an arrow function params group,
60
+ // so continue scanning to capture the body too.
61
+ const rest = src.slice(i + 1);
62
+ const arrowMatch = rest.match(/^[ \t]*=>/);
63
+ if (arrowMatch) {
64
+ i += 1 + arrowMatch[0].length;
65
+ while (i < src.length && /[ \t]/.test(src[i]!)) i++;
66
+ // Reset depth — next balanced group is the body
67
+ continue;
68
+ }
69
+ return src.slice(exprStart, i + 1).trim();
70
+ }
71
+ } else if (depth === 0 && (ch === ',' || ch === ';' || ch === '\n')) {
72
+ break;
73
+ }
74
+ i++;
75
+ }
76
+
77
+ if (i > exprStart) return src.slice(exprStart, i).trim();
78
+ return null;
79
+ }
80
+
81
+ export function mergeHandlers(
82
+ doc: ServerDoc,
83
+ existing: Map<string, string>,
84
+ ): { doc: ServerDoc; stubbed: string[] } {
85
+ const stubbed: string[] = [];
86
+
87
+ const tools = doc.tools.map((tool) => {
88
+ if (tool.handler !== undefined) return tool;
89
+ const found = existing.get(tool.name);
90
+ if (found !== undefined) return { ...tool, handler: found };
91
+ stubbed.push(tool.name);
92
+ return {
93
+ ...tool,
94
+ handler: `async () => ({ content: [{ type: 'text', text: 'TODO: implement ${tool.name}' }] })`,
95
+ };
96
+ });
97
+
98
+ return { doc: { ...doc, tools }, stubbed };
99
+ }
@@ -0,0 +1,236 @@
1
+ import { readdirSync, readFileSync } from 'fs';
2
+ import { join } from 'path';
3
+ import type { ServerDoc, ToolDoc, PromptDoc, ResourceDoc } from './types.js';
4
+
5
+ function extractSections(content: string): Map<string, string> {
6
+ const sections = new Map<string, string>();
7
+ const pieces = ('\n' + content).split('\n## ');
8
+ for (const piece of pieces) {
9
+ if (!piece.trim()) continue;
10
+ const nlIdx = piece.indexOf('\n');
11
+ if (nlIdx === -1) continue;
12
+ const name = piece.slice(0, nlIdx).trim();
13
+ const body = piece.slice(nlIdx + 1);
14
+ sections.set(name, body);
15
+ }
16
+ return sections;
17
+ }
18
+
19
+ function extractFencedBlock(body: string, lang: string): string | null {
20
+ // Match ``` + lang followed immediately by a newline to avoid matching
21
+ // a longer lang identifier (e.g. ```json when looking for ```js).
22
+ const startMarker = '```' + lang + '\n';
23
+ const start = body.indexOf(startMarker);
24
+ if (start === -1) return null;
25
+ const contentStart = start + startMarker.length;
26
+ const end = body.indexOf('\n```', contentStart);
27
+ if (end === -1) return body.slice(contentStart).trim();
28
+ return body.slice(contentStart, end).trim();
29
+ }
30
+
31
+ export function parseKvBlock(body: string): Record<string, string> {
32
+ const result: Record<string, string> = {};
33
+ for (const line of body.split('\n')) {
34
+ const colonIdx = line.indexOf(':');
35
+ if (colonIdx === -1) continue;
36
+ const key = line.slice(0, colonIdx).trim();
37
+ const value = line.slice(colonIdx + 1).trim();
38
+ if (key) result[key] = value;
39
+ }
40
+ return result;
41
+ }
42
+
43
+ function parseFrontmatter(block: string): Record<string, string> {
44
+ return parseKvBlock(block);
45
+ }
46
+
47
+ function stripHandlerMarkers(jsContent: string): string {
48
+ return jsContent
49
+ .split('\n')
50
+ .filter((l) => !l.trim().startsWith('// @handler:') && !l.trim().startsWith('// @end-handler:'))
51
+ .join('\n')
52
+ .trim();
53
+ }
54
+
55
+ function parseToolsSection(body: string): ToolDoc[] {
56
+ const tools: ToolDoc[] = [];
57
+ const pieces = ('\n' + body).split('\n### ');
58
+ for (const piece of pieces.slice(1)) {
59
+ const nlIdx = piece.indexOf('\n');
60
+ const name = (nlIdx === -1 ? piece : piece.slice(0, nlIdx)).trim();
61
+ const rest = nlIdx === -1 ? '' : piece.slice(nlIdx + 1);
62
+
63
+ const descMatch = rest.match(/\*\*Description:\*\*\s*(.+)/);
64
+ const description = descMatch?.[1]?.trim() ?? '';
65
+
66
+ const schemaStr = extractFencedBlock(rest, 'json');
67
+ const schema = schemaStr
68
+ ? (JSON.parse(schemaStr) as ToolDoc['schema'])
69
+ : {};
70
+
71
+ const jsBlock = extractFencedBlock(rest, 'js');
72
+ // Strip @handler/@end-handler markers so ToolDoc.handler holds only the function source.
73
+ const handler = jsBlock !== null ? stripHandlerMarkers(jsBlock) : undefined;
74
+
75
+ tools.push({ name, description, schema, handler });
76
+ }
77
+ return tools;
78
+ }
79
+
80
+ function parseResourcesSection(body: string): ResourceDoc[] {
81
+ const resources: ResourceDoc[] = [];
82
+ const blocks = body.split('\n---\n');
83
+ for (let i = 0; i < blocks.length; i += 2) {
84
+ const frontmatterBlock = (blocks[i] ?? '').trim();
85
+ if (!frontmatterBlock) continue;
86
+ const bodyBlock = blocks[i + 1] ?? '';
87
+ const frontmatter = parseFrontmatter(frontmatterBlock);
88
+ if (!frontmatter['uri']) continue;
89
+ resources.push({
90
+ uri: frontmatter['uri'],
91
+ name: frontmatter['name'],
92
+ mimeType: frontmatter['mimeType'],
93
+ content: bodyBlock.trim(),
94
+ });
95
+ }
96
+ return resources;
97
+ }
98
+
99
+ function parseToolFile(content: string): ToolDoc {
100
+ const lines = content.split('\n');
101
+
102
+ const h1Line = lines.find((l) => l.startsWith('# '));
103
+ const name = h1Line ? h1Line.slice(2).trim() : '';
104
+
105
+ let h1Seen = false;
106
+ let description = '';
107
+ for (const line of lines) {
108
+ if (line.startsWith('# ')) { h1Seen = true; continue; }
109
+ if (!h1Seen) continue;
110
+ if (line.startsWith('#')) break;
111
+ const trimmed = line.trim();
112
+ if (trimmed) { description = trimmed; break; }
113
+ }
114
+
115
+ const sections = extractSections(content);
116
+
117
+ const parametersBody = sections.get('Parameters') ?? '';
118
+ const schema: ToolDoc['schema'] = {};
119
+ for (const line of parametersBody.split('\n')) {
120
+ if (!line.startsWith('- ')) continue;
121
+ const m = line.match(/^- (\w+) \(([^)]+)\): (.+)/);
122
+ if (!m) continue;
123
+ const pName = m[1]!;
124
+ const typeStr = m[2]!.split(',')[0]!.trim() as 'string' | 'number' | 'boolean';
125
+ const desc = m[3]!.trim();
126
+ schema[pName] = { type: typeStr, description: desc };
127
+ }
128
+
129
+ const handlerBody = sections.get('Handler') ?? '';
130
+ const handler = extractFencedBlock(handlerBody, 'js') ?? undefined;
131
+
132
+ return { name, description, schema, handler };
133
+ }
134
+
135
+ function parsePromptFile(content: string): PromptDoc {
136
+ const lines = content.split('\n');
137
+
138
+ const h1Line = lines.find((l) => l.startsWith('# '));
139
+ const name = h1Line ? h1Line.slice(2).trim() : '';
140
+
141
+ let h1Seen = false;
142
+ let description = '';
143
+ for (const line of lines) {
144
+ if (line.startsWith('# ')) { h1Seen = true; continue; }
145
+ if (!h1Seen) continue;
146
+ if (line.startsWith('#')) break;
147
+ const trimmed = line.trim();
148
+ if (trimmed) { description = trimmed; break; }
149
+ }
150
+
151
+ const sections = extractSections(content);
152
+
153
+ const argsBody = sections.get('Args') ?? '';
154
+ const args: PromptDoc['args'] = {};
155
+ for (const line of argsBody.split('\n')) {
156
+ if (!line.startsWith('- ')) continue;
157
+ const m = line.match(/^- (\w+) \(([^)]+)\): .+/);
158
+ if (!m) continue;
159
+ args[m[1]!] = { type: m[2]!.trim() };
160
+ }
161
+
162
+ const messageBody = sections.get('Message') ?? '';
163
+ const messages: PromptDoc['messages'] = [];
164
+ for (const line of messageBody.split('\n')) {
165
+ if (!line.startsWith('> ')) continue;
166
+ const colonIdx = line.indexOf(': ', 2);
167
+ if (colonIdx === -1) continue;
168
+ const role = line.slice(2, colonIdx).trim();
169
+ const msgContent = line.slice(colonIdx + 2).trim();
170
+ messages.push({ role, content: msgContent });
171
+ }
172
+
173
+ return { name, description, args, messages };
174
+ }
175
+
176
+ export function parseServerDoc(content: string): ServerDoc {
177
+ const sections = extractSections(content);
178
+
179
+ const configBody = sections.get('Config') ?? '';
180
+ const configStr = extractFencedBlock(configBody, 'json') ?? '{}';
181
+ const config = JSON.parse(configStr) as ServerDoc['config'];
182
+
183
+ const toolsBody = sections.get('Tools') ?? '';
184
+ const tools = parseToolsSection(toolsBody);
185
+
186
+ const promptsBody = sections.get('Prompts') ?? '';
187
+ const promptsStr = extractFencedBlock(promptsBody, 'json') ?? '[]';
188
+ const prompts = JSON.parse(promptsStr) as PromptDoc[];
189
+
190
+ const resourcesBody = sections.get('Resources') ?? '';
191
+ const resources = parseResourcesSection(resourcesBody);
192
+
193
+ return { config, tools, prompts, resources };
194
+ }
195
+
196
+ export function parseServerDir(dir: string): ServerDoc {
197
+ const serverMdContent = readFileSync(join(dir, 'server.md'), 'utf-8');
198
+ const sections = extractSections(serverMdContent);
199
+
200
+ const configBody = sections.get('Config') ?? '';
201
+ const configKv = parseKvBlock(configBody);
202
+ const config: ServerDoc['config'] = {
203
+ name: configKv['name'] ?? '',
204
+ transport: (configKv['transport'] ?? 'stdio') as 'stdio' | 'http',
205
+ ...(configKv['port'] ? { port: parseInt(configKv['port'], 10) } : {}),
206
+ };
207
+
208
+ const resourcesBody = sections.get('Resources') ?? '';
209
+ const resources = parseResourcesSection(resourcesBody);
210
+
211
+ const toolsDir = join(dir, 'tools');
212
+ const tools: ToolDoc[] = [];
213
+ let toolFiles: string[] = [];
214
+ try {
215
+ toolFiles = readdirSync(toolsDir).filter((f) => f.endsWith('.md'));
216
+ } catch {
217
+ // tools/ dir doesn't exist — no tools
218
+ }
219
+ for (const file of toolFiles) {
220
+ tools.push(parseToolFile(readFileSync(join(toolsDir, file), 'utf-8')));
221
+ }
222
+
223
+ const promptsDir = join(dir, 'prompts');
224
+ const prompts: PromptDoc[] = [];
225
+ let promptFiles: string[] = [];
226
+ try {
227
+ promptFiles = readdirSync(promptsDir).filter((f) => f.endsWith('.md'));
228
+ } catch {
229
+ // prompts/ dir doesn't exist — no prompts
230
+ }
231
+ for (const file of promptFiles) {
232
+ prompts.push(parsePromptFile(readFileSync(join(promptsDir, file), 'utf-8')));
233
+ }
234
+
235
+ return { config, tools, prompts, resources };
236
+ }
@@ -0,0 +1,100 @@
1
+ import { writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import type { ServerDoc, ResourceDoc } from './types.js';
4
+
5
+ function serializeResources(resources: ResourceDoc[]): string {
6
+ return resources
7
+ .map((r) => {
8
+ let fm = `uri: ${r.uri}`;
9
+ if (r.name) fm += `\nname: ${r.name}`;
10
+ if (r.mimeType) fm += `\nmimeType: ${r.mimeType}`;
11
+ return `${fm}\n---\n${r.content}`;
12
+ })
13
+ .join('\n---\n');
14
+ }
15
+
16
+ export function serializeServerDoc(doc: ServerDoc): string {
17
+ const configSection =
18
+ `## Config\n\`\`\`json\n${JSON.stringify(doc.config, null, 2)}\n\`\`\``;
19
+
20
+ const toolParts = doc.tools.map((tool) => {
21
+ let part = `### ${tool.name}\n**Description:** ${tool.description}\n\n`;
22
+ part += `\`\`\`json\n${JSON.stringify(tool.schema, null, 2)}\n\`\`\``;
23
+ if (tool.handler !== undefined) {
24
+ part +=
25
+ `\n\n\`\`\`js\n// @handler:${tool.name}\n${tool.handler}\n// @end-handler:${tool.name}\n\`\`\``;
26
+ }
27
+ return part;
28
+ });
29
+ const toolsSection =
30
+ doc.tools.length > 0
31
+ ? `## Tools\n\n${toolParts.join('\n\n')}`
32
+ : `## Tools`;
33
+
34
+ const promptsSection =
35
+ `## Prompts\n\`\`\`json\n${JSON.stringify(doc.prompts, null, 2)}\n\`\`\``;
36
+
37
+ const resourcesSection =
38
+ doc.resources.length > 0
39
+ ? `## Resources\n${serializeResources(doc.resources)}`
40
+ : `## Resources`;
41
+
42
+ return [configSection, toolsSection, promptsSection, resourcesSection].join('\n\n') + '\n';
43
+ }
44
+
45
+ export function serializeServerDir(doc: ServerDoc, dir: string): void {
46
+ mkdirSync(join(dir, 'tools'), { recursive: true });
47
+ mkdirSync(join(dir, 'prompts'), { recursive: true });
48
+
49
+ let serverMd = '## Config\n';
50
+ serverMd += `name: ${doc.config.name}\n`;
51
+ serverMd += `transport: ${doc.config.transport}\n`;
52
+ if (doc.config.port !== undefined) serverMd += `port: ${doc.config.port}\n`;
53
+
54
+ if (doc.resources.length > 0) {
55
+ serverMd += `\n## Resources\n${serializeResources(doc.resources)}\n`;
56
+ } else {
57
+ serverMd += '\n## Resources\n';
58
+ }
59
+
60
+ writeFileSync(join(dir, 'server.md'), serverMd);
61
+ console.log('✓ server.md updated');
62
+
63
+ for (const tool of doc.tools) {
64
+ let md = `# ${tool.name}\n\n${tool.description}\n\n## Parameters\n\n`;
65
+ for (const [pName, pDef] of Object.entries(tool.schema)) {
66
+ md += `- ${pName} (${pDef.type}): ${pDef.description ?? pName}\n`;
67
+ }
68
+ if (tool.handler !== undefined) {
69
+ const cleanHandler = tool.handler
70
+ .split('\n')
71
+ .filter((l) => !l.trim().startsWith('// @handler:') && !l.trim().startsWith('// @end-handler:'))
72
+ .join('\n')
73
+ .trim();
74
+ md += `\n## Handler\n\n\`\`\`js\n${cleanHandler}\n\`\`\`\n`;
75
+ }
76
+ writeFileSync(join(dir, 'tools', `${tool.name}.md`), md);
77
+ }
78
+
79
+ for (const prompt of doc.prompts) {
80
+ let md = `# ${prompt.name}\n\n`;
81
+ if (prompt.description) md += `${prompt.description}\n\n`;
82
+ if (Object.keys(prompt.args).length > 0) {
83
+ md += '## Args\n\n';
84
+ for (const [argName, argDef] of Object.entries(prompt.args)) {
85
+ md += `- ${argName} (${argDef.type}): ${argName}\n`;
86
+ }
87
+ md += '\n';
88
+ }
89
+ if (prompt.messages.length > 0) {
90
+ md += '## Message\n\n';
91
+ for (const msg of prompt.messages) {
92
+ md += `> ${msg.role}: ${msg.content}\n`;
93
+ }
94
+ }
95
+ writeFileSync(join(dir, 'prompts', `${prompt.name}.md`), md);
96
+ }
97
+
98
+ console.log(`✓ tools/ updated (${doc.tools.length} files)`);
99
+ console.log(`✓ prompts/ updated (${doc.prompts.length} files)`);
100
+ }
@@ -0,0 +1,27 @@
1
+ export type ToolDoc = {
2
+ name: string;
3
+ description: string;
4
+ schema: Record<string, { type: 'string' | 'number' | 'boolean'; description?: string }>;
5
+ handler?: string;
6
+ };
7
+
8
+ export type PromptDoc = {
9
+ name: string;
10
+ description?: string;
11
+ args: Record<string, { type: string }>;
12
+ messages: Array<{ role: string; content: string }>;
13
+ };
14
+
15
+ export type ResourceDoc = {
16
+ uri: string;
17
+ name?: string;
18
+ mimeType?: string;
19
+ content: string;
20
+ };
21
+
22
+ export type ServerDoc = {
23
+ config: { name: string; transport: 'stdio' | 'http'; port?: number };
24
+ tools: ToolDoc[];
25
+ prompts: PromptDoc[];
26
+ resources: ResourceDoc[];
27
+ };
@@ -0,0 +1,25 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+
4
+ export type CliConfig = {
5
+ name: string;
6
+ transport: "stdio" | "http";
7
+ port: number;
8
+ };
9
+
10
+ const defaults: CliConfig = {
11
+ name: "mcpill-server",
12
+ transport: "stdio",
13
+ port: 3333,
14
+ };
15
+
16
+ export async function loadConfig(mcpillDir: string): Promise<CliConfig> {
17
+ const configPath = path.join(mcpillDir, "mcpill.config.json");
18
+ if (!fs.existsSync(configPath)) {
19
+ return { ...defaults };
20
+ }
21
+ const raw = JSON.parse(
22
+ fs.readFileSync(configPath, "utf-8")
23
+ ) as Partial<CliConfig>;
24
+ return { ...defaults, ...raw };
25
+ }