mcpill 1.3.0 → 1.6.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.
@@ -1,105 +0,0 @@
1
- import path from "path";
2
- import fs from "fs";
3
- import { z } from "mcpill-runtime";
4
- import { createServer, applySetup } 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 mcpillDir = path.join(baseDir, ".mcpill", "server");
23
- if (!fs.existsSync(path.join(mcpillDir, "mcpill.config.json"))) {
24
- console.error("No pill found — run mcpill compile first");
25
- process.exit(1);
26
- };
27
-
28
- const [tools, prompts, resources, config] = await Promise.all([
29
- loadTools(mcpillDir),
30
- loadPrompts(mcpillDir),
31
- loadResources(mcpillDir),
32
- loadConfig(mcpillDir),
33
- ]);
34
-
35
- const transport = opts.transport ?? config.transport;
36
- const port = opts.port ?? config.port;
37
- const { name } = config;
38
-
39
- const server = createServer({
40
- name,
41
- version: "0.1.0",
42
- transport,
43
- http: { port },
44
- });
45
-
46
- for (const { name: toolName, description, schema, handler } of tools) {
47
- server.defineTool({
48
- name: toolName,
49
- description,
50
- schema: z.object(schema as ZodRawShape),
51
- handler: handler as (input: Record<string, unknown>) => Promise<unknown>,
52
- });
53
- }
54
-
55
- for (const { name: promptName, description, messages } of prompts) {
56
- server.definePrompt({
57
- name: promptName,
58
- description,
59
- handler: async (args: Record<string, string>) =>
60
- messages
61
- .map((m) =>
62
- m.content.replace(
63
- /\{\{(\w+)\}\}/g,
64
- (_, k: string) => String(args[k] ?? "")
65
- )
66
- )
67
- .join("\n"),
68
- });
69
- }
70
-
71
- for (const { uri, name: resourceName, content } of resources) {
72
- server.defineResource({
73
- uri,
74
- description: resourceName ?? uri,
75
- resolver: async () => content,
76
- });
77
- }
78
-
79
- applySetup(name, tools.map((t) => t.name), {
80
- projectPath: baseDir,
81
- permissions: 'restrictive',
82
- register: true,
83
- cmdOverride: {
84
- command: 'mcpill',
85
- args: ['run', '--dir', baseDir],
86
- },
87
- })
88
-
89
- try {
90
- await server.start();
91
- } catch (err) {
92
- const nodeErr = err as NodeJS.ErrnoException;
93
- if (nodeErr.code === "EADDRINUSE") {
94
- console.error(
95
- `Port ${port} is already in use. Use --port to specify another.`
96
- );
97
- process.exit(1);
98
- }
99
- throw err;
100
- }
101
-
102
- process.stderr.write(
103
- `✓ MCPill running — ${tools.length} tools, ${prompts.length} prompts, ${resources.length} resources [${transport}]\n`
104
- );
105
- }
@@ -1,87 +0,0 @@
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
- import { parseAgentMdTools } from "../compiler/hooks.js";
8
-
9
- async function validateOne(mcpillDir: string): Promise<void> {
10
- const errors: string[] = [];
11
- let toolCount = 0;
12
- let promptCount = 0;
13
- let resourceCount = 0;
14
-
15
- try {
16
- const tools = await loadTools(mcpillDir);
17
- toolCount = tools.length;
18
- } catch (err) {
19
- errors.push(err instanceof Error ? err.message : String(err));
20
- }
21
-
22
- try {
23
- const prompts = await loadPrompts(mcpillDir);
24
- promptCount = prompts.length;
25
- } catch (err) {
26
- errors.push(err instanceof Error ? err.message : String(err));
27
- }
28
-
29
- try {
30
- const resources = await loadResources(mcpillDir);
31
- resourceCount = resources.length;
32
- } catch (err) {
33
- errors.push(err instanceof Error ? err.message : String(err));
34
- }
35
-
36
- try {
37
- await loadConfig(mcpillDir);
38
- } catch (err) {
39
- errors.push(err instanceof Error ? err.message : String(err));
40
- }
41
-
42
- if (errors.length > 0) {
43
- for (const error of errors) {
44
- console.error(error);
45
- }
46
- process.exit(1);
47
- }
48
-
49
- console.log(`✓ Valid: ${toolCount} tools, ${promptCount} prompts, ${resourceCount} resources`);
50
- }
51
-
52
- export async function runValidate(baseDir: string): Promise<void> {
53
- const mcpillDir = path.join(baseDir, ".mcpill");
54
- const serverDir = path.join(mcpillDir, "server");
55
- if (!fs.existsSync(path.join(serverDir, "mcpill.config.json"))) {
56
- console.error("No pill directories found — run mcpill compile first");
57
- process.exit(1);
58
- }
59
-
60
- await validateOne(serverDir);
61
-
62
- // Non-fatal: warn if AGENT.md declares replacements but no hook is present
63
- if (!fs.existsSync(path.join(mcpillDir, ".no-hooks"))) {
64
- const agentMdPath = fs.existsSync(path.join(baseDir, "AGENT.md"))
65
- ? path.join(baseDir, "AGENT.md")
66
- : fs.existsSync(path.join(mcpillDir, "AGENT.md"))
67
- ? path.join(mcpillDir, "AGENT.md")
68
- : null;
69
-
70
- if (agentMdPath) {
71
- const tools = parseAgentMdTools(fs.readFileSync(agentMdPath, "utf-8"));
72
- if (tools.length > 0) {
73
- const settingsPath = path.join(baseDir, ".claude", "settings.json");
74
- let hasHook = false;
75
- if (fs.existsSync(settingsPath)) {
76
- const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")) as {
77
- hooks?: { PreToolUse?: unknown[] };
78
- };
79
- hasHook = Array.isArray(settings?.hooks?.PreToolUse) && settings.hooks.PreToolUse.length > 0;
80
- }
81
- if (!hasHook) {
82
- console.warn("⚠ No PreToolUse hook in .claude/settings.json — run mcpill compile to add it");
83
- }
84
- }
85
- }
86
- }
87
- }
@@ -1,140 +0,0 @@
1
- import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'fs';
2
- import { join } from 'path';
3
- import { parseKvBlock } from './parse.js';
4
-
5
- export interface AgentTool {
6
- name: string;
7
- replaces: string[];
8
- }
9
-
10
- export interface HookEntry {
11
- matcher: string;
12
- hooks: Array<{ type: string; command: string }>;
13
- }
14
-
15
- function parseMdTableRow(line: string): string[] {
16
- return line
17
- .split('|')
18
- .slice(1, -1)
19
- .map((c) => c.trim());
20
- }
21
-
22
- export function parsePillMdTools(content: string): AgentTool[] {
23
- const tools: AgentTool[] = [];
24
- const pieces = ('\n' + content).split('\n## ');
25
- for (const piece of pieces) {
26
- if (!piece.toLowerCase().startsWith('tool:')) continue;
27
- const nlIdx = piece.indexOf('\n');
28
- const sectionTitle = piece.slice(0, nlIdx === -1 ? undefined : nlIdx).trim();
29
- const toolName = sectionTitle.slice('tool:'.length).trim();
30
- if (!toolName) continue;
31
- const body = nlIdx === -1 ? '' : piece.slice(nlIdx + 1);
32
- const kv = parseKvBlock(body);
33
- const replaces = (kv['replaces'] ?? '')
34
- .split(',')
35
- .map((s) => s.trim())
36
- .filter(Boolean);
37
- if (replaces.length > 0) {
38
- tools.push({ name: toolName, replaces });
39
- }
40
- }
41
- return tools;
42
- }
43
-
44
- export function parseAgentMdTools(content: string): AgentTool[] {
45
- const sections = ('\n' + content).split(/\n## /);
46
- const toolsSection = sections.find((s) => /^Tools\s*\n/.test(s));
47
- if (!toolsSection) return [];
48
-
49
- const lines = toolsSection.split('\n');
50
- let headerIdx = -1;
51
- let replacesColIdx = -1;
52
-
53
- for (let i = 0; i < lines.length; i++) {
54
- const line = lines[i]!.trim();
55
- if (!line.startsWith('|')) continue;
56
- const cells = parseMdTableRow(line);
57
- const idx = cells.findIndex((c) => c.toLowerCase() === 'replaces');
58
- if (idx !== -1) {
59
- headerIdx = i;
60
- replacesColIdx = idx;
61
- break;
62
- }
63
- }
64
- if (headerIdx === -1) return [];
65
-
66
- const tools: AgentTool[] = [];
67
- for (let i = headerIdx + 1; i < lines.length; i++) {
68
- const line = lines[i]!.trim();
69
- if (!line.startsWith('|')) break;
70
- // Skip separator rows like |---|---|
71
- if (/^\|[\s\-:|]+\|$/.test(line)) continue;
72
-
73
- const cells = parseMdTableRow(line);
74
- const name = cells[0];
75
- if (!name) continue;
76
-
77
- const replacesCell = cells[replacesColIdx] ?? '';
78
- const replaces = replacesCell
79
- .split(',')
80
- .map((s) => s.trim())
81
- .filter(Boolean);
82
-
83
- if (replaces.length > 0) {
84
- tools.push({ name, replaces });
85
- }
86
- }
87
-
88
- return tools;
89
- }
90
-
91
- export function buildHookEntry(pillName: string, tools: AgentTool[]): HookEntry | null {
92
- const nativeToolSet = new Set<string>();
93
- for (const tool of tools) {
94
- for (const native of tool.replaces) {
95
- nativeToolSet.add(native);
96
- }
97
- }
98
- if (nativeToolSet.size === 0) return null;
99
-
100
- const summary = tools.map((t) => `${t.name}→${t.replaces.join(',')}`).join('; ');
101
- const matcher = Array.from(nativeToolSet).join('|');
102
- const message = `[mcpill:${pillName}] Use pill tools instead: ${summary}. See .mcpill/AGENT.md.`;
103
-
104
- return {
105
- matcher,
106
- hooks: [{ type: 'command', command: `echo ${JSON.stringify(message)}` }],
107
- };
108
- }
109
-
110
- interface SettingsJson {
111
- hooks?: {
112
- PreToolUse?: HookEntry[];
113
- [key: string]: unknown;
114
- };
115
- [key: string]: unknown;
116
- }
117
-
118
- export function mergeHookIntoSettings(baseDir: string, hookEntry: HookEntry): void {
119
- const claudeDir = join(baseDir, '.claude');
120
- const settingsPath = join(claudeDir, 'settings.json');
121
-
122
- let settings: SettingsJson = {};
123
- if (existsSync(settingsPath)) {
124
- settings = JSON.parse(readFileSync(settingsPath, 'utf-8')) as SettingsJson;
125
- }
126
-
127
- if (!settings.hooks) settings.hooks = {};
128
- if (!settings.hooks.PreToolUse) settings.hooks.PreToolUse = [];
129
-
130
- // Merge by matcher identity: replace existing entry, otherwise append
131
- const idx = settings.hooks.PreToolUse.findIndex((e) => e.matcher === hookEntry.matcher);
132
- if (idx !== -1) {
133
- settings.hooks.PreToolUse[idx] = hookEntry;
134
- } else {
135
- settings.hooks.PreToolUse.push(hookEntry);
136
- }
137
-
138
- mkdirSync(claudeDir, { recursive: true });
139
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
140
- }
@@ -1,99 +0,0 @@
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
- }
@@ -1,236 +0,0 @@
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, 'server', '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, 'server', '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
- }