@sdods/mcp 0.2.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 (43) hide show
  1. package/LICENSE +17 -0
  2. package/dist/.tsbuildinfo +1 -0
  3. package/dist/cli.d.ts +50 -0
  4. package/dist/cli.js +171 -0
  5. package/dist/fs.d.ts +45 -0
  6. package/dist/fs.js +184 -0
  7. package/dist/index.d.ts +15 -0
  8. package/dist/index.js +15 -0
  9. package/dist/install/index.d.ts +46 -0
  10. package/dist/install/index.js +146 -0
  11. package/dist/prompts/index.d.ts +33 -0
  12. package/dist/prompts/index.js +151 -0
  13. package/dist/proposals.d.ts +71 -0
  14. package/dist/proposals.js +188 -0
  15. package/dist/registry/adapters.d.ts +46 -0
  16. package/dist/registry/adapters.js +76 -0
  17. package/dist/registry/registry.d.ts +96 -0
  18. package/dist/registry/registry.js +141 -0
  19. package/dist/resources.d.ts +5 -0
  20. package/dist/resources.js +130 -0
  21. package/dist/server.d.ts +29 -0
  22. package/dist/server.js +143 -0
  23. package/dist/tools/analyze.d.ts +87 -0
  24. package/dist/tools/analyze.js +820 -0
  25. package/dist/tools/data.d.ts +18 -0
  26. package/dist/tools/data.js +153 -0
  27. package/dist/tools/feature.d.ts +53 -0
  28. package/dist/tools/feature.js +340 -0
  29. package/dist/tools/index.d.ts +12 -0
  30. package/dist/tools/index.js +23 -0
  31. package/dist/tools/issue-schedule.d.ts +22 -0
  32. package/dist/tools/issue-schedule.js +193 -0
  33. package/dist/tools/project.d.ts +11 -0
  34. package/dist/tools/project.js +144 -0
  35. package/dist/tools/proposal.d.ts +19 -0
  36. package/dist/tools/proposal.js +89 -0
  37. package/dist/tools/run.d.ts +49 -0
  38. package/dist/tools/run.js +387 -0
  39. package/dist/transports/http.d.ts +40 -0
  40. package/dist/transports/http.js +151 -0
  41. package/dist/transports/stdio.d.ts +6 -0
  42. package/dist/transports/stdio.js +15 -0
  43. package/package.json +57 -0
@@ -0,0 +1,188 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from 'node:fs';
2
+ import { dirname, join, relative, resolve } from 'node:path';
3
+ import { newId } from '@sdods/contracts';
4
+ import { PROPOSALS_DIR, safeJoin } from './fs.js';
5
+ export class ProposalStore {
6
+ rootDir;
7
+ dir;
8
+ constructor(rootDir) {
9
+ this.rootDir = rootDir;
10
+ this.dir = resolve(rootDir, PROPOSALS_DIR);
11
+ }
12
+ manifestPath(id) {
13
+ return join(this.dir, id, 'manifest.json');
14
+ }
15
+ create(input) {
16
+ const stamp = new Date()
17
+ .toISOString()
18
+ .replace(/[-:]/g, '')
19
+ .replace(/\..*/, '')
20
+ .replace('T', '-');
21
+ const id = `${stamp}-${input.role}-${newId().slice(-6)}`;
22
+ const pdir = join(this.dir, id);
23
+ mkdirSync(join(pdir, 'files'), { recursive: true });
24
+ const files = [];
25
+ for (const f of input.files) {
26
+ const op = f.op ?? (existsSync(resolve(this.rootDir, f.path)) ? 'modify' : 'add');
27
+ // validate the target path stays inside the repo
28
+ safeJoin(this.rootDir, f.path);
29
+ if (op === 'delete') {
30
+ files.push({ path: f.path, op });
31
+ continue;
32
+ }
33
+ const target = safeJoin(join(pdir, 'files'), f.path);
34
+ mkdirSync(dirname(target), { recursive: true });
35
+ writeFileSync(target, f.content ?? '');
36
+ files.push({ path: f.path, op, from: `files/${f.path}` });
37
+ }
38
+ const manifest = {
39
+ id,
40
+ role: input.role,
41
+ project: input.project,
42
+ env: input.env,
43
+ createdAt: new Date().toISOString(),
44
+ status: 'pending',
45
+ summary: input.summary,
46
+ costUsd: input.costUsd,
47
+ model: input.model,
48
+ files,
49
+ verification: input.verification ?? { status: 'not-run' },
50
+ };
51
+ writeFileSync(this.manifestPath(id), JSON.stringify(manifest, null, 2));
52
+ return manifest;
53
+ }
54
+ list(filter = {}) {
55
+ if (!existsSync(this.dir))
56
+ return [];
57
+ return readdirSync(this.dir)
58
+ .filter((id) => existsSync(this.manifestPath(id)))
59
+ .map((id) => this.get(id))
60
+ .filter((m) => Boolean(m))
61
+ .filter((m) => (filter.status ? m.status === filter.status : true))
62
+ .filter((m) => (filter.project ? m.project === filter.project : true))
63
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt));
64
+ }
65
+ get(id) {
66
+ const p = this.manifestPath(id);
67
+ if (!existsSync(p))
68
+ return undefined;
69
+ return JSON.parse(readFileSync(p, 'utf8'));
70
+ }
71
+ readFile(id, file) {
72
+ if (!file.from)
73
+ return undefined;
74
+ const p = safeJoin(join(this.dir, id), file.from);
75
+ return existsSync(p) ? readFileSync(p, 'utf8') : undefined;
76
+ }
77
+ /** Unified-ish diff for review (no external dependency). */
78
+ diff(id) {
79
+ const m = this.get(id);
80
+ if (!m)
81
+ throw new Error(`Unknown proposal ${id}`);
82
+ const parts = [];
83
+ for (const f of m.files) {
84
+ const target = resolve(this.rootDir, f.path);
85
+ const before = f.op !== 'add' && existsSync(target) ? readFileSync(target, 'utf8') : '';
86
+ const after = f.op === 'delete' ? '' : (this.readFile(id, f) ?? '');
87
+ parts.push(`--- ${f.op === 'add' ? '/dev/null' : `a/${f.path}`}\n+++ ${f.op === 'delete' ? '/dev/null' : `b/${f.path}`}`);
88
+ parts.push(simpleDiff(before, after));
89
+ }
90
+ return parts.join('\n');
91
+ }
92
+ /** Apply to `targetRoot` (default: the repo). Returns the written paths. */
93
+ accept(id, opts = {}) {
94
+ const m = this.get(id);
95
+ if (!m)
96
+ throw new Error(`Unknown proposal ${id}`);
97
+ if (m.status === 'rejected')
98
+ throw new Error(`Proposal ${id} was rejected`);
99
+ const root = opts.targetRoot ?? this.rootDir;
100
+ const written = [];
101
+ for (const f of m.files) {
102
+ const target = safeJoin(root, f.path);
103
+ if (f.op === 'delete') {
104
+ if (existsSync(target))
105
+ rmSync(target);
106
+ written.push(f.path);
107
+ continue;
108
+ }
109
+ const src = safeJoin(join(this.dir, id), f.from);
110
+ mkdirSync(dirname(target), { recursive: true });
111
+ copyFileSync(src, target);
112
+ written.push(f.path);
113
+ }
114
+ m.status = 'accepted';
115
+ m.reviewedAt = new Date().toISOString();
116
+ m.reviewedBy = opts.reviewedBy;
117
+ m.appliedTo = relative(this.rootDir, root) || '.';
118
+ writeFileSync(this.manifestPath(id), JSON.stringify(m, null, 2));
119
+ return written;
120
+ }
121
+ reject(id, reason, reviewedBy) {
122
+ const m = this.get(id);
123
+ if (!m)
124
+ throw new Error(`Unknown proposal ${id}`);
125
+ m.status = 'rejected';
126
+ m.reviewedAt = new Date().toISOString();
127
+ m.reviewedBy = reviewedBy;
128
+ m.verification = { ...(m.verification ?? {}), notes: reason ?? m.verification?.notes };
129
+ writeFileSync(this.manifestPath(id), JSON.stringify(m, null, 2));
130
+ return m;
131
+ }
132
+ sizeOf(id) {
133
+ const dir = join(this.dir, id, 'files');
134
+ if (!existsSync(dir))
135
+ return 0;
136
+ let total = 0;
137
+ const stack = [dir];
138
+ while (stack.length) {
139
+ const cur = stack.pop();
140
+ for (const n of readdirSync(cur)) {
141
+ const p = join(cur, n);
142
+ const st = statSync(p);
143
+ if (st.isDirectory())
144
+ stack.push(p);
145
+ else
146
+ total += st.size;
147
+ }
148
+ }
149
+ return total;
150
+ }
151
+ }
152
+ /** Line-based diff with +/- markers (LCS on lines, adequate for review output). */
153
+ export function simpleDiff(before, after) {
154
+ const a = before === '' ? [] : before.split('\n');
155
+ const b = after === '' ? [] : after.split('\n');
156
+ const n = a.length;
157
+ const m = b.length;
158
+ const dp = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
159
+ for (let i = n - 1; i >= 0; i--) {
160
+ for (let j = m - 1; j >= 0; j--) {
161
+ dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
162
+ }
163
+ }
164
+ const out = [];
165
+ let i = 0;
166
+ let j = 0;
167
+ while (i < n && j < m) {
168
+ if (a[i] === b[j]) {
169
+ out.push(` ${a[i]}`);
170
+ i++;
171
+ j++;
172
+ }
173
+ else if (dp[i + 1][j] >= dp[i][j + 1]) {
174
+ out.push(`-${a[i]}`);
175
+ i++;
176
+ }
177
+ else {
178
+ out.push(`+${b[j]}`);
179
+ j++;
180
+ }
181
+ }
182
+ while (i < n)
183
+ out.push(`-${a[i++]}`);
184
+ while (j < m)
185
+ out.push(`+${b[j++]}`);
186
+ return out.join('\n');
187
+ }
188
+ //# sourceMappingURL=proposals.js.map
@@ -0,0 +1,46 @@
1
+ import { z } from 'zod';
2
+ import type { McpServer } from '@modelcontextprotocol/server';
3
+ import type { ToolRegistry } from './registry.js';
4
+ import { type SdodsTool, type ToolContext } from './registry.js';
5
+ /** Register every visible tool on an MCP v2 server. */
6
+ export declare function toMcpServer(reg: ToolRegistry, server: McpServer, ctx: ToolContext): number;
7
+ /** Plain tool definitions the agents package wraps with the Claude Agent SDK `tool()` helper. */
8
+ export interface AgentSdkToolDef {
9
+ name: string;
10
+ description: string;
11
+ inputSchema: Record<string, z.ZodTypeAny>;
12
+ /**
13
+ * Parameters as JSON Schema, for a tool that did not come from a zod shape — a tool bridged
14
+ * from another MCP server, whose schema is already JSON Schema. When set it wins.
15
+ */
16
+ parametersJsonSchema?: Record<string, unknown>;
17
+ annotations: {
18
+ readOnlyHint: boolean;
19
+ destructiveHint: boolean;
20
+ };
21
+ handler: (args: Record<string, unknown>) => Promise<{
22
+ content: Array<{
23
+ type: 'text';
24
+ text: string;
25
+ } | {
26
+ type: 'image';
27
+ data: string;
28
+ mimeType: string;
29
+ }>;
30
+ isError?: boolean;
31
+ }>;
32
+ }
33
+ export declare function toAgentSdkTools(reg: ToolRegistry, ctx: ToolContext): AgentSdkToolDef[];
34
+ /** OpenAI-compatible function definitions (chat completions `tools`). */
35
+ export interface OpenAiFunctionDef {
36
+ type: 'function';
37
+ function: {
38
+ name: string;
39
+ description: string;
40
+ parameters: Record<string, unknown>;
41
+ };
42
+ }
43
+ export declare function toOpenAiFunctions(reg: ToolRegistry, ctx?: Pick<ToolContext, 'principal' | 'caps'>): OpenAiFunctionDef[];
44
+ /** JSON schema view of a tool (used by docs generation and tests). */
45
+ export declare function toolJsonSchema(tool: SdodsTool<any>): Record<string, unknown>;
46
+ //# sourceMappingURL=adapters.d.ts.map
@@ -0,0 +1,76 @@
1
+ import { z } from 'zod';
2
+ import {} from './registry.js';
3
+ /** Register every visible tool on an MCP v2 server. */
4
+ export function toMcpServer(reg, server, ctx) {
5
+ let count = 0;
6
+ for (const tool of reg.list(ctx)) {
7
+ server.registerTool(tool.name, {
8
+ title: tool.title,
9
+ description: tool.description,
10
+ inputSchema: tool.shape,
11
+ annotations: {
12
+ readOnlyHint: tool.annotations?.readOnlyHint ?? tool.access === 'read',
13
+ destructiveHint: tool.annotations?.destructiveHint ?? false,
14
+ idempotentHint: tool.annotations?.idempotentHint ?? tool.access === 'read',
15
+ openWorldHint: tool.annotations?.openWorldHint ?? false,
16
+ },
17
+ _meta: { access: tool.access, capability: tool.capability },
18
+ }, async (args, extra) => {
19
+ const serverCtx = extra;
20
+ const callCtx = {
21
+ ...ctx,
22
+ signal: serverCtx?.signal ?? ctx.signal,
23
+ onProgress: (p) => {
24
+ ctx.onProgress?.(p);
25
+ void serverCtx?.mcpReq
26
+ ?.log?.('info', p.message ?? `${p.current ?? ''}/${p.total ?? ''}`)
27
+ .catch(() => undefined);
28
+ },
29
+ };
30
+ const result = await reg.call(tool.name, args, callCtx);
31
+ return result;
32
+ });
33
+ count++;
34
+ }
35
+ return count;
36
+ }
37
+ /** Upper bound for one structured tool result inside an agent conversation. */
38
+ const STRUCTURED_LIMIT = 4000;
39
+ function clip(json) {
40
+ return json.length <= STRUCTURED_LIMIT
41
+ ? json
42
+ : `${json.slice(0, STRUCTURED_LIMIT)}… (${json.length - STRUCTURED_LIMIT} more chars; narrow the query or read the file directly)`;
43
+ }
44
+ export function toAgentSdkTools(reg, ctx) {
45
+ return reg.list(ctx).map((tool) => ({
46
+ name: tool.name,
47
+ description: tool.description,
48
+ inputSchema: tool.shape,
49
+ annotations: { readOnlyHint: tool.access === 'read', destructiveHint: false },
50
+ handler: async (args) => {
51
+ const r = await reg.call(tool.name, args, ctx);
52
+ const content = [...r.content];
53
+ // The text content is already clipped by `summarize`; appending the whole structured payload
54
+ // would put it straight back and grow the conversation without bound, so it is clipped too.
55
+ if (r.structuredContent)
56
+ content.push({ type: 'text', text: clip(JSON.stringify(r.structuredContent)) });
57
+ return { content, isError: r.isError };
58
+ },
59
+ }));
60
+ }
61
+ export function toOpenAiFunctions(reg, ctx) {
62
+ const tools = ctx ? reg.list(ctx) : reg.all();
63
+ return tools.map((tool) => ({
64
+ type: 'function',
65
+ function: {
66
+ name: tool.name,
67
+ description: tool.description,
68
+ parameters: z.toJSONSchema(z.object(tool.shape)),
69
+ },
70
+ }));
71
+ }
72
+ /** JSON schema view of a tool (used by docs generation and tests). */
73
+ export function toolJsonSchema(tool) {
74
+ return z.toJSONSchema(z.object(tool.shape));
75
+ }
76
+ //# sourceMappingURL=adapters.js.map
@@ -0,0 +1,96 @@
1
+ import { z, type ZodRawShape } from 'zod';
2
+ import { type Scope } from '@sdods/contracts';
3
+ export declare const DOCS_BASE_URL = "https://docs.sdods.com";
4
+ export type ToolAccess = 'read' | 'run' | 'write';
5
+ export type Capability = 'core' | 'analyze' | 'run' | 'data' | 'agents' | 'issues' | 'schedules';
6
+ export declare const ALL_CAPABILITIES: Capability[];
7
+ export declare const DEFAULT_CAPABILITIES: Capability[];
8
+ export interface Principal {
9
+ userId?: string;
10
+ name?: string;
11
+ scopes: string[];
12
+ via: 'stdio' | 'http';
13
+ }
14
+ export interface ToolLogger {
15
+ info(msg: string, data?: unknown): void;
16
+ warn(msg: string, data?: unknown): void;
17
+ debug(msg: string, data?: unknown): void;
18
+ }
19
+ export interface ToolContext {
20
+ rootDir: string;
21
+ cwd: string;
22
+ principal: Principal;
23
+ caps: Set<Capability>;
24
+ /** default project/env when the client did not pass one */
25
+ project?: string;
26
+ env?: string;
27
+ onProgress?: (progress: {
28
+ message?: string;
29
+ current?: number;
30
+ total?: number;
31
+ }) => void;
32
+ signal?: AbortSignal;
33
+ logger: ToolLogger;
34
+ }
35
+ export interface ToolAnnotations {
36
+ readOnlyHint?: boolean;
37
+ destructiveHint?: boolean;
38
+ idempotentHint?: boolean;
39
+ openWorldHint?: boolean;
40
+ }
41
+ export interface ToolResult {
42
+ /** human readable summary (markdown) */
43
+ text: string;
44
+ /** machine readable payload */
45
+ data?: unknown;
46
+ isError?: boolean;
47
+ /** optional images (base64 PNG) */
48
+ images?: Array<{
49
+ data: string;
50
+ mimeType: string;
51
+ }>;
52
+ }
53
+ export interface SdodsTool<S extends ZodRawShape = ZodRawShape> {
54
+ name: string;
55
+ title: string;
56
+ description: string;
57
+ shape: S;
58
+ access: ToolAccess;
59
+ /** scope domain: projects, features, runs, datasets, agents, integrations, schedules … */
60
+ domain: string;
61
+ capability: Capability;
62
+ annotations?: ToolAnnotations;
63
+ docsPath?: string;
64
+ handler: (args: z.infer<z.ZodObject<S>>, ctx: ToolContext) => Promise<ToolResult>;
65
+ }
66
+ export interface McpToolResultShape {
67
+ content: Array<{
68
+ type: 'text';
69
+ text: string;
70
+ } | {
71
+ type: 'image';
72
+ data: string;
73
+ mimeType: string;
74
+ }>;
75
+ structuredContent?: Record<string, unknown>;
76
+ isError?: boolean;
77
+ _meta?: Record<string, unknown>;
78
+ }
79
+ export declare function defineTool<S extends ZodRawShape>(tool: SdodsTool<S>): SdodsTool<S>;
80
+ export declare function requiredScope(tool: SdodsTool<any>): Scope | null;
81
+ export declare class ToolRegistry {
82
+ private readonly tools;
83
+ register<S extends ZodRawShape>(tool: SdodsTool<S>): this;
84
+ registerAll(tools: Array<SdodsTool<any>>): this;
85
+ get(name: string): SdodsTool<any> | undefined;
86
+ all(): SdodsTool<any>[];
87
+ /** Tools visible to a principal with the given capabilities (scope + capability gating). */
88
+ list(ctx: Pick<ToolContext, 'principal' | 'caps'>): SdodsTool<any>[];
89
+ isAllowed(tool: SdodsTool<any>, ctx: Pick<ToolContext, 'principal' | 'caps'>): boolean;
90
+ call(name: string, rawArgs: unknown, ctx: ToolContext): Promise<McpToolResultShape>;
91
+ }
92
+ export declare function toMcpResult(tool: SdodsTool<any>, result: ToolResult): McpToolResultShape;
93
+ export declare function errorResult(message: string, data?: Record<string, unknown>): McpToolResultShape;
94
+ /** Summarise a JSON payload for the text part of a result. */
95
+ export declare function summarize(title: string, data: unknown, max?: number): string;
96
+ //# sourceMappingURL=registry.d.ts.map
@@ -0,0 +1,141 @@
1
+ import { z } from 'zod';
2
+ import { hasScope, isScope, scopeForToolAccess } from '@sdods/contracts';
3
+ export const DOCS_BASE_URL = 'https://docs.sdods.com';
4
+ export const ALL_CAPABILITIES = [
5
+ 'core',
6
+ 'analyze',
7
+ 'run',
8
+ 'data',
9
+ 'agents',
10
+ 'issues',
11
+ 'schedules',
12
+ ];
13
+ export const DEFAULT_CAPABILITIES = ['core', 'analyze', 'run', 'data', 'schedules'];
14
+ export function defineTool(tool) {
15
+ return tool;
16
+ }
17
+ export function requiredScope(tool) {
18
+ // agents has no read/write pair: run → agents:run, review/apply → agents:review
19
+ if (tool.domain === 'agents')
20
+ return tool.access === 'run' ? 'agents:run' : 'agents:review';
21
+ return scopeForToolAccess(tool.access, tool.domain);
22
+ }
23
+ export class ToolRegistry {
24
+ tools = new Map();
25
+ register(tool) {
26
+ if (this.tools.has(tool.name))
27
+ throw new Error(`Tool already registered: ${tool.name}`);
28
+ this.tools.set(tool.name, tool);
29
+ return this;
30
+ }
31
+ registerAll(tools) {
32
+ for (const t of tools)
33
+ this.register(t);
34
+ return this;
35
+ }
36
+ get(name) {
37
+ return this.tools.get(name);
38
+ }
39
+ all() {
40
+ return [...this.tools.values()];
41
+ }
42
+ /** Tools visible to a principal with the given capabilities (scope + capability gating). */
43
+ list(ctx) {
44
+ return this.all().filter((t) => this.isAllowed(t, ctx));
45
+ }
46
+ isAllowed(tool, ctx) {
47
+ if (!ctx.caps.has(tool.capability))
48
+ return false;
49
+ const scope = requiredScope(tool);
50
+ if (!scope)
51
+ return true;
52
+ if (!isScope(scope))
53
+ return true;
54
+ return hasScope(ctx.principal.scopes, scope);
55
+ }
56
+ async call(name, rawArgs, ctx) {
57
+ const tool = this.tools.get(name);
58
+ if (!tool) {
59
+ return errorResult(`Unknown tool: ${name}`, { code: 'UNKNOWN_TOOL' });
60
+ }
61
+ if (!ctx.caps.has(tool.capability)) {
62
+ return errorResult(`Tool ${name} requires capability "${tool.capability}" (start with --caps ${tool.capability}).`, {
63
+ code: 'CAPABILITY_DISABLED',
64
+ });
65
+ }
66
+ const scope = requiredScope(tool);
67
+ if (scope && isScope(scope) && !hasScope(ctx.principal.scopes, scope)) {
68
+ return errorResult(`Insufficient scope: ${name} requires ${scope}.`, {
69
+ code: 'INSUFFICIENT_SCOPE',
70
+ scope,
71
+ });
72
+ }
73
+ const parsed = z.object(tool.shape).safeParse(rawArgs ?? {});
74
+ if (!parsed.success) {
75
+ return errorResult(`Invalid arguments for ${name}: ${parsed.error.issues.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`).join('; ')}`, {
76
+ code: 'INVALID_ARGS',
77
+ issues: parsed.error.issues,
78
+ });
79
+ }
80
+ try {
81
+ const result = await tool.handler(parsed.data, ctx);
82
+ return toMcpResult(tool, result);
83
+ }
84
+ catch (e) {
85
+ const err = e;
86
+ ctx.logger.warn(`tool ${name} failed`, { message: err.message });
87
+ return errorResult(err.message, {
88
+ code: err.error?.code ?? 'TOOL_FAILED',
89
+ hint: err.error?.hint,
90
+ exitCode: err.exitCode,
91
+ });
92
+ }
93
+ }
94
+ }
95
+ export function toMcpResult(tool, result) {
96
+ const content = [{ type: 'text', text: result.text }];
97
+ for (const img of result.images ?? [])
98
+ content.push({ type: 'image', data: img.data, mimeType: img.mimeType });
99
+ const structured = toStructured(result.data);
100
+ return {
101
+ content,
102
+ ...(structured ? { structuredContent: structured } : {}),
103
+ ...(result.isError ? { isError: true } : {}),
104
+ _meta: {
105
+ docsUrl: `${DOCS_BASE_URL}${tool.docsPath ?? '/docs/reference/mcp-tools'}#${tool.name.replace(/_/g, '-')}`,
106
+ access: tool.access,
107
+ capability: tool.capability,
108
+ },
109
+ };
110
+ }
111
+ function toStructured(data) {
112
+ if (data === undefined)
113
+ return undefined;
114
+ if (Array.isArray(data))
115
+ return { items: data };
116
+ if (data && typeof data === 'object')
117
+ return data;
118
+ return { value: data };
119
+ }
120
+ export function errorResult(message, data = {}) {
121
+ const code = typeof data.code === 'string' ? data.code : 'TOOL_FAILED';
122
+ return {
123
+ content: [
124
+ {
125
+ type: 'text',
126
+ text: `Error [${code}]: ${message}${data.hint ? `\nHint: ${String(data.hint)}` : ''}`,
127
+ },
128
+ ],
129
+ structuredContent: { error: { message, ...data } },
130
+ isError: true,
131
+ };
132
+ }
133
+ /** Summarise a JSON payload for the text part of a result. */
134
+ export function summarize(title, data, max = 4000) {
135
+ const body = typeof data === 'string' ? data : JSON.stringify(data, null, 2);
136
+ const clipped = body.length > max
137
+ ? body.slice(0, max) + `\n… (${body.length - max} more chars in structuredContent)`
138
+ : body;
139
+ return `${title}\n\n\`\`\`json\n${clipped}\n\`\`\``;
140
+ }
141
+ //# sourceMappingURL=registry.js.map
@@ -0,0 +1,5 @@
1
+ import type { McpServer } from '@modelcontextprotocol/server';
2
+ import type { ToolContext } from './registry/registry.js';
3
+ /** MCP resources: features, run summaries, NDJSON, screenshots and proposals. */
4
+ export declare function registerResources(server: McpServer, ctx: ToolContext): void;
5
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1,130 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { ResourceTemplate } from '@modelcontextprotocol/server';
4
+ import { runFiles } from '@sdods/contracts';
5
+ import { getRun, listRuns, projectRoot, safeJoin, walk } from './fs.js';
6
+ import { ProposalStore } from './proposals.js';
7
+ /** MCP resources: features, run summaries, NDJSON, screenshots and proposals. */
8
+ export function registerResources(server, ctx) {
9
+ server.registerResource('feature', new ResourceTemplate('sdods://project/{slug}/features/{+path}', {
10
+ list: async () => {
11
+ const resources = [];
12
+ const projectsDir = join(ctx.rootDir, process.env.SDODS_PROJECTS_DIR ?? 'projects');
13
+ for (const f of walk(projectsDir, (x) => x.endsWith('.feature'), 500)) {
14
+ const rel = f.slice(projectsDir.length + 1).replace(/\\/g, '/');
15
+ const [slug, ...rest] = rel.split('/');
16
+ if (!slug || rest[0] !== 'features')
17
+ continue;
18
+ resources.push({
19
+ uri: `sdods://project/${slug}/features/${rest.slice(1).join('/')}`,
20
+ name: rel,
21
+ mimeType: 'text/x-gherkin',
22
+ });
23
+ }
24
+ return { resources };
25
+ },
26
+ }), {
27
+ title: 'Feature file',
28
+ description: 'Gherkin feature files per project',
29
+ mimeType: 'text/x-gherkin',
30
+ }, async (uri, variables) => {
31
+ const slug = String(variables.slug);
32
+ const path = String(variables.path);
33
+ const file = safeJoin(join(projectRoot(ctx.rootDir, slug), 'features'), path);
34
+ if (!existsSync(file))
35
+ throw new Error(`No such feature: ${path}`);
36
+ return {
37
+ contents: [{ uri: uri.href, mimeType: 'text/x-gherkin', text: readFileSync(file, 'utf8') }],
38
+ };
39
+ });
40
+ server.registerResource('run-summary', new ResourceTemplate('sdods://run/{runId}/summary', {
41
+ list: async () => ({
42
+ resources: listRuns(ctx.rootDir, 50).map((r) => ({
43
+ uri: `sdods://run/${r.runId}/summary`,
44
+ name: `run ${r.runId}`,
45
+ mimeType: 'application/json',
46
+ })),
47
+ }),
48
+ }), {
49
+ title: 'Run summary',
50
+ description: 'run.json + summary.json of a run',
51
+ mimeType: 'application/json',
52
+ }, async (uri, variables) => {
53
+ const run = getRun(ctx.rootDir, String(variables.runId));
54
+ if (!run)
55
+ throw new Error(`Unknown run ${String(variables.runId)}`);
56
+ return {
57
+ contents: [
58
+ {
59
+ uri: uri.href,
60
+ mimeType: 'application/json',
61
+ text: JSON.stringify({ manifest: run.manifest, summary: run.summary }, null, 2),
62
+ },
63
+ ],
64
+ };
65
+ });
66
+ server.registerResource('run-messages', new ResourceTemplate('sdods://run/{runId}/messages.ndjson', { list: undefined }), {
67
+ title: 'Cucumber messages',
68
+ description: 'NDJSON messages of a run',
69
+ mimeType: 'application/x-ndjson',
70
+ }, async (uri, variables) => {
71
+ const run = getRun(ctx.rootDir, String(variables.runId));
72
+ const file = run ? join(run.dir, runFiles.messages) : undefined;
73
+ if (!file || !existsSync(file))
74
+ throw new Error(`No messages for run ${String(variables.runId)}`);
75
+ return {
76
+ contents: [
77
+ { uri: uri.href, mimeType: 'application/x-ndjson', text: readFileSync(file, 'utf8') },
78
+ ],
79
+ };
80
+ });
81
+ server.registerResource('screenshot', new ResourceTemplate('sdods://screenshot/{runId}/{fingerprint}/{retry}/{file}', {
82
+ list: undefined,
83
+ }), {
84
+ title: 'Screenshot',
85
+ description: 'Before/after and scenario screenshots of a run',
86
+ mimeType: 'image/png',
87
+ }, async (uri, variables) => {
88
+ const run = getRun(ctx.rootDir, String(variables.runId));
89
+ if (!run)
90
+ throw new Error(`Unknown run ${String(variables.runId)}`);
91
+ const slug = run.manifest?.projectSlug;
92
+ const base = slug ? join(run.dir, slug) : run.dir;
93
+ const file = safeJoin(base, `${String(variables.fingerprint)}/r${String(variables.retry)}/${String(variables.file)}`);
94
+ if (!existsSync(file))
95
+ throw new Error(`No such screenshot: ${String(variables.file)}`);
96
+ return {
97
+ contents: [
98
+ { uri: uri.href, mimeType: 'image/png', blob: readFileSync(file).toString('base64') },
99
+ ],
100
+ };
101
+ });
102
+ server.registerResource('proposal', new ResourceTemplate('sdods://proposal/{id}', {
103
+ list: async () => ({
104
+ resources: new ProposalStore(ctx.rootDir).list().map((p) => ({
105
+ uri: `sdods://proposal/${p.id}`,
106
+ name: p.summary,
107
+ mimeType: 'application/json',
108
+ })),
109
+ }),
110
+ }), {
111
+ title: 'Proposal',
112
+ description: 'Agent proposal manifest and diff',
113
+ mimeType: 'application/json',
114
+ }, async (uri, variables) => {
115
+ const store = new ProposalStore(ctx.rootDir);
116
+ const m = store.get(String(variables.id));
117
+ if (!m)
118
+ throw new Error(`Unknown proposal ${String(variables.id)}`);
119
+ return {
120
+ contents: [
121
+ {
122
+ uri: uri.href,
123
+ mimeType: 'application/json',
124
+ text: JSON.stringify({ ...m, diff: store.diff(m.id) }, null, 2),
125
+ },
126
+ ],
127
+ };
128
+ });
129
+ }
130
+ //# sourceMappingURL=resources.js.map