@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,193 @@
1
+ import { join } from 'node:path';
2
+ import { z } from 'zod';
3
+ import { SdodsCliError, sdodsCli, cliOrNote } from '../cli.js';
4
+ import { projectRoot, readYaml } from '../fs.js';
5
+ import { defineTool, summarize } from '../registry/registry.js';
6
+ export const issueTools = [
7
+ defineTool({
8
+ name: 'issue_create',
9
+ title: 'Create an issue for a failed run',
10
+ description: 'Create (or comment on an existing) GitHub/Jira issue for the failures of a run via `sdods integrations notify`. Use dryRun to preview.',
11
+ shape: {
12
+ runId: z.string(),
13
+ project: z.string().optional(),
14
+ provider: z.enum(['github', 'jira']).optional(),
15
+ dryRun: z.boolean().optional(),
16
+ },
17
+ access: 'write',
18
+ domain: 'integrations',
19
+ capability: 'issues',
20
+ annotations: { openWorldHint: true },
21
+ handler: async (args, ctx) => {
22
+ const cli = ['integrations', 'notify', '--run-id', args.runId, '--from-files'];
23
+ if (args.project)
24
+ cli.push('-p', args.project);
25
+ if (args.provider)
26
+ cli.push('--provider', args.provider);
27
+ if (args.dryRun)
28
+ cli.push('--dry-run');
29
+ const data = await cliOrNote(cli, {
30
+ cwd: ctx.rootDir,
31
+ signal: ctx.signal,
32
+ timeoutMs: 120_000,
33
+ });
34
+ return {
35
+ text: summarize(`Issue ${args.dryRun ? 'preview' : 'creation'} for run ${args.runId}`, data),
36
+ data,
37
+ };
38
+ },
39
+ }),
40
+ defineTool({
41
+ name: 'issue_link',
42
+ title: 'List issue links',
43
+ description: 'Scenario ↔ issue links (@jira:KEY / @github:N tags and auto-created issues) for a project.',
44
+ shape: { project: z.string() },
45
+ access: 'read',
46
+ domain: 'integrations',
47
+ capability: 'issues',
48
+ handler: async (args, ctx) => {
49
+ const data = await cliOrNote(['integrations', 'links', '-p', args.project], {
50
+ cwd: ctx.rootDir,
51
+ signal: ctx.signal,
52
+ });
53
+ return { text: summarize(`Issue links for ${args.project}`, data), data };
54
+ },
55
+ }),
56
+ defineTool({
57
+ name: 'issue_sync',
58
+ title: 'Sync issue statuses',
59
+ description: 'Scan features for @jira:/@github: tags, create missing links and refresh statuses from the trackers.',
60
+ shape: { project: z.string() },
61
+ access: 'write',
62
+ domain: 'integrations',
63
+ capability: 'issues',
64
+ annotations: { openWorldHint: true },
65
+ handler: async (args, ctx) => {
66
+ try {
67
+ const r = await sdodsCli(['integrations', 'sync', '-p', args.project], {
68
+ cwd: ctx.rootDir,
69
+ signal: ctx.signal,
70
+ timeoutMs: 120_000,
71
+ });
72
+ return { text: summarize(`Synced ${args.project}`, r.json), data: r.json };
73
+ }
74
+ catch (e) {
75
+ if (e instanceof SdodsCliError)
76
+ return { text: `Sync failed: ${e.message}`, data: e.error, isError: true };
77
+ throw e;
78
+ }
79
+ },
80
+ }),
81
+ ];
82
+ export const scheduleTools = [
83
+ defineTool({
84
+ name: 'schedule_list',
85
+ title: 'List schedules',
86
+ description: 'Cron schedules declared in a project yaml (and the processes that carry a schedule).',
87
+ shape: { project: z.string() },
88
+ access: 'read',
89
+ domain: 'schedules',
90
+ capability: 'schedules',
91
+ handler: async (args, ctx) => {
92
+ const root = projectRoot(ctx.rootDir, args.project);
93
+ const yaml = readYaml(join(root, 'sdods.project.yaml'));
94
+ const ws = readYaml(join(ctx.rootDir, 'sdods.workspace.yaml'));
95
+ const scheduledProcesses = [...(ws?.defaults?.processes ?? []), ...(yaml?.processes ?? [])]
96
+ .filter((p) => p.schedule)
97
+ .map((p) => ({ process: p.name, cron: p.schedule, trigger: p.trigger }));
98
+ const data = { schedules: yaml?.schedules ?? [], scheduledProcesses };
99
+ return { text: summarize(`Schedules of ${args.project}`, data), data };
100
+ },
101
+ }),
102
+ defineTool({
103
+ name: 'schedule_next',
104
+ title: 'Next fire times',
105
+ description: 'Preview the next fire times of a cron expression in a timezone (5-field cron).',
106
+ shape: {
107
+ cron: z.string(),
108
+ timezone: z.string().optional(),
109
+ count: z.number().int().positive().max(20).optional(),
110
+ },
111
+ access: 'read',
112
+ domain: 'schedules',
113
+ capability: 'schedules',
114
+ handler: async (args) => {
115
+ const times = nextCronTimes(args.cron, args.count ?? 5, args.timezone ?? 'UTC');
116
+ return {
117
+ text: summarize(`Next ${times.length} fire times for "${args.cron}" (${args.timezone ?? 'UTC'})`, times),
118
+ data: { times },
119
+ };
120
+ },
121
+ }),
122
+ ];
123
+ /** Minimal 5-field cron evaluator (minute hour dom month dow) — good enough for previews. */
124
+ export function nextCronTimes(expr, count, timeZone, from = new Date()) {
125
+ const fields = expr.trim().split(/\s+/);
126
+ if (fields.length !== 5)
127
+ throw Object.assign(new Error(`Expected 5 cron fields, got ${fields.length}`), {
128
+ error: { code: 'INVALID_ARGS' },
129
+ });
130
+ const parse = (f, min, max) => {
131
+ const out = new Set();
132
+ for (const part of f.split(',')) {
133
+ const [rangePart, stepPart] = part.split('/');
134
+ const step = stepPart ? Number(stepPart) : 1;
135
+ let lo = min;
136
+ let hi = max;
137
+ if (rangePart !== '*') {
138
+ const [a, b] = rangePart.split('-').map(Number);
139
+ lo = a;
140
+ hi = b ?? (stepPart ? max : a);
141
+ }
142
+ for (let v = lo; v <= hi; v += step)
143
+ out.add(v);
144
+ }
145
+ return out;
146
+ };
147
+ const [mi, ho, dom, mo, dow] = [
148
+ parse(fields[0], 0, 59),
149
+ parse(fields[1], 0, 23),
150
+ parse(fields[2], 1, 31),
151
+ parse(fields[3], 1, 12),
152
+ parse(fields[4], 0, 7),
153
+ ];
154
+ if (dow.has(7))
155
+ dow.add(0);
156
+ const fmt = new Intl.DateTimeFormat('en-US', {
157
+ timeZone,
158
+ hourCycle: 'h23',
159
+ year: 'numeric',
160
+ month: 'numeric',
161
+ day: 'numeric',
162
+ hour: 'numeric',
163
+ minute: 'numeric',
164
+ weekday: 'short',
165
+ });
166
+ const parts = (d) => {
167
+ const p = Object.fromEntries(fmt.formatToParts(d).map((x) => [x.type, x.value]));
168
+ const wd = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(p.weekday);
169
+ return {
170
+ minute: Number(p.minute),
171
+ hour: Number(p.hour),
172
+ day: Number(p.day),
173
+ month: Number(p.month),
174
+ weekday: wd,
175
+ };
176
+ };
177
+ const out = [];
178
+ const cursor = new Date(from);
179
+ cursor.setUTCSeconds(0, 0);
180
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
181
+ for (let i = 0; i < 366 * 24 * 60 && out.length < count; i++) {
182
+ const p = parts(cursor);
183
+ if (mi.has(p.minute) &&
184
+ ho.has(p.hour) &&
185
+ dom.has(p.day) &&
186
+ mo.has(p.month) &&
187
+ dow.has(p.weekday))
188
+ out.push(cursor.toISOString());
189
+ cursor.setUTCMinutes(cursor.getUTCMinutes() + 1);
190
+ }
191
+ return out;
192
+ }
193
+ //# sourceMappingURL=issue-schedule.js.map
@@ -0,0 +1,11 @@
1
+ import { z } from 'zod';
2
+ export declare const projectTools: (import("../registry/registry.js").SdodsTool<{}> | import("../registry/registry.js").SdodsTool<{
3
+ project: z.ZodString;
4
+ env: z.ZodOptional<z.ZodString>;
5
+ explain: z.ZodOptional<z.ZodBoolean>;
6
+ }> | import("../registry/registry.js").SdodsTool<{
7
+ project: z.ZodString;
8
+ }> | import("../registry/registry.js").SdodsTool<{
9
+ project: z.ZodOptional<z.ZodString>;
10
+ }>)[];
11
+ //# sourceMappingURL=project.d.ts.map
@@ -0,0 +1,144 @@
1
+ import { z } from 'zod';
2
+ import { sdodsCli, cliOrNote } from '../cli.js';
3
+ import { defineTool, summarize } from '../registry/registry.js';
4
+ export const projectTools = [
5
+ defineTool({
6
+ name: 'project_list',
7
+ title: 'List projects',
8
+ description: 'List SDODS projects discovered under projects/ with layers, browsers and environments.',
9
+ shape: {},
10
+ access: 'read',
11
+ domain: 'projects',
12
+ capability: 'core',
13
+ docsPath: '/docs/reference/mcp-tools',
14
+ handler: async (_args, ctx) => {
15
+ const r = await sdodsCli(['project', 'list'], {
16
+ cwd: ctx.rootDir,
17
+ signal: ctx.signal,
18
+ });
19
+ return { text: summarize('Projects', r.json), data: r.json };
20
+ },
21
+ }),
22
+ defineTool({
23
+ name: 'project_get_config',
24
+ title: 'Get resolved project config',
25
+ description: 'Resolved configuration for a project and environment (defaults → project yaml → env yaml → .env → process → CLI). Secrets are redacted. Use explain=true to see which layer produced each key.',
26
+ shape: {
27
+ project: z.string().describe('project slug'),
28
+ env: z.string().optional().describe('environment name (default: project envs.default)'),
29
+ explain: z.boolean().optional().describe('return provenance rows instead of the tree'),
30
+ },
31
+ access: 'read',
32
+ domain: 'projects',
33
+ capability: 'core',
34
+ handler: async (args, ctx) => {
35
+ const cli = ['config', 'show', '-p', args.project];
36
+ if (args.env)
37
+ cli.push('-e', args.env);
38
+ if (args.explain)
39
+ cli.push('--explain');
40
+ const r = await sdodsCli(cli, { cwd: ctx.rootDir, signal: ctx.signal });
41
+ return {
42
+ text: summarize(`Config for ${args.project}${args.env ? ` (${args.env})` : ''}`, r.json),
43
+ data: r.json,
44
+ };
45
+ },
46
+ }),
47
+ defineTool({
48
+ name: 'project_list_envs',
49
+ title: 'List environments',
50
+ description: 'Environments of a project with UI/API base URLs and whether they resolve without errors.',
51
+ shape: { project: z.string() },
52
+ access: 'read',
53
+ domain: 'envs',
54
+ capability: 'core',
55
+ handler: async (args, ctx) => {
56
+ const r = await sdodsCli(['env', 'list', '-p', args.project], {
57
+ cwd: ctx.rootDir,
58
+ signal: ctx.signal,
59
+ });
60
+ return { text: summarize(`Environments of ${args.project}`, r.json), data: r.json };
61
+ },
62
+ }),
63
+ defineTool({
64
+ name: 'project_validate_config',
65
+ title: 'Validate all configs',
66
+ description: 'Validate every project and environment yaml without running anything.',
67
+ shape: {},
68
+ access: 'read',
69
+ domain: 'projects',
70
+ capability: 'core',
71
+ handler: async (_args, ctx) => {
72
+ let data;
73
+ try {
74
+ data = (await sdodsCli(['config', 'validate'], { cwd: ctx.rootDir, signal: ctx.signal }))
75
+ .json;
76
+ }
77
+ catch (e) {
78
+ // exit code 2 still prints the table as JSON
79
+ const err = e;
80
+ return {
81
+ text: `Validation reported problems: ${err.message}`,
82
+ data: { error: err.message },
83
+ isError: true,
84
+ };
85
+ }
86
+ return { text: summarize('Configuration validation', data), data };
87
+ },
88
+ }),
89
+ defineTool({
90
+ name: 'project_doctor',
91
+ title: 'Doctor',
92
+ description: 'Check Node, Bun, browser engines, projects, env vars and database settings.',
93
+ shape: { project: z.string().optional() },
94
+ access: 'read',
95
+ domain: 'projects',
96
+ capability: 'core',
97
+ annotations: { openWorldHint: true },
98
+ handler: async (args, ctx) => {
99
+ const cli = ['doctor', ...(args.project ? ['-p', args.project] : [])];
100
+ let data;
101
+ try {
102
+ data = (await sdodsCli(cli, { cwd: ctx.rootDir, signal: ctx.signal, timeoutMs: 60_000 }))
103
+ .json;
104
+ }
105
+ catch (e) {
106
+ const err = e;
107
+ // doctor exits 1 when a check fails but still prints JSON on stdout
108
+ const m = /\[[\s\S]*\]/.exec(err.stderr ?? '');
109
+ data = m ? JSON.parse(m[0]) : { error: err.message };
110
+ }
111
+ return { text: summarize('Doctor', data), data };
112
+ },
113
+ }),
114
+ defineTool({
115
+ name: 'workspace_tree',
116
+ title: 'Workspace tree',
117
+ description: 'Hierarchy: organization → workspaces → projects → modules (testing types, tags) → processes.',
118
+ shape: {},
119
+ access: 'read',
120
+ domain: 'workspaces',
121
+ capability: 'core',
122
+ handler: async (_args, ctx) => {
123
+ const data = await cliOrNote(['workspace', 'tree'], { cwd: ctx.rootDir, signal: ctx.signal });
124
+ return { text: summarize('Workspace tree', data), data };
125
+ },
126
+ }),
127
+ defineTool({
128
+ name: 'process_list',
129
+ title: 'List processes',
130
+ description: 'Named run recipes (pr-check, nightly-regression, release-gate …) visible to a project: trigger, tags, layers, browsers, gates.',
131
+ shape: { project: z.string() },
132
+ access: 'read',
133
+ domain: 'processes',
134
+ capability: 'core',
135
+ handler: async (args, ctx) => {
136
+ const data = await cliOrNote(['processes', 'list', '-p', args.project], {
137
+ cwd: ctx.rootDir,
138
+ signal: ctx.signal,
139
+ });
140
+ return { text: summarize(`Processes for ${args.project}`, data), data };
141
+ },
142
+ }),
143
+ ];
144
+ //# sourceMappingURL=project.js.map
@@ -0,0 +1,19 @@
1
+ import { z } from 'zod';
2
+ export declare const proposalTools: (import("../registry/registry.js").SdodsTool<{
3
+ status: z.ZodOptional<z.ZodEnum<{
4
+ pending: "pending";
5
+ accepted: "accepted";
6
+ rejected: "rejected";
7
+ }>>;
8
+ project: z.ZodOptional<z.ZodString>;
9
+ }> | import("../registry/registry.js").SdodsTool<{
10
+ id: z.ZodString;
11
+ includeDiff: z.ZodOptional<z.ZodBoolean>;
12
+ }> | import("../registry/registry.js").SdodsTool<{
13
+ id: z.ZodString;
14
+ reviewedBy: z.ZodOptional<z.ZodString>;
15
+ }> | import("../registry/registry.js").SdodsTool<{
16
+ id: z.ZodString;
17
+ reason: z.ZodOptional<z.ZodString>;
18
+ }>)[];
19
+ //# sourceMappingURL=proposal.d.ts.map
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod';
2
+ import { ProposalStore } from '../proposals.js';
3
+ import { defineTool, summarize } from '../registry/registry.js';
4
+ export const proposalTools = [
5
+ defineTool({
6
+ name: 'proposal_list',
7
+ title: 'List proposals',
8
+ description: 'Pending/accepted/rejected proposals written by agents and write tools.',
9
+ shape: {
10
+ status: z.enum(['pending', 'accepted', 'rejected']).optional(),
11
+ project: z.string().optional(),
12
+ },
13
+ access: 'read',
14
+ domain: 'features',
15
+ capability: 'core',
16
+ handler: async (args, ctx) => {
17
+ const rows = new ProposalStore(ctx.rootDir).list(args).map((m) => ({
18
+ id: m.id,
19
+ role: m.role,
20
+ project: m.project,
21
+ status: m.status,
22
+ summary: m.summary,
23
+ files: m.files.length,
24
+ createdAt: m.createdAt,
25
+ costUsd: m.costUsd,
26
+ }));
27
+ return { text: summarize(`Proposals (${rows.length})`, rows), data: rows };
28
+ },
29
+ }),
30
+ defineTool({
31
+ name: 'proposal_get',
32
+ title: 'Get proposal',
33
+ description: 'Manifest, files and a diff against the working tree for one proposal.',
34
+ shape: { id: z.string(), includeDiff: z.boolean().optional() },
35
+ access: 'read',
36
+ domain: 'features',
37
+ capability: 'core',
38
+ handler: async (args, ctx) => {
39
+ const store = new ProposalStore(ctx.rootDir);
40
+ const m = store.get(args.id);
41
+ if (!m)
42
+ throw Object.assign(new Error(`Unknown proposal ${args.id}`), {
43
+ error: { code: 'NOT_FOUND' },
44
+ });
45
+ const files = m.files.map((f) => ({
46
+ ...f,
47
+ content: f.op === 'delete' ? undefined : store.readFile(args.id, f),
48
+ }));
49
+ const diff = args.includeDiff === false ? undefined : store.diff(args.id);
50
+ return {
51
+ text: `${m.summary}\n\n${diff ? `\`\`\`diff\n${diff.slice(0, 6000)}\n\`\`\`` : ''}`,
52
+ data: { ...m, files, diff },
53
+ };
54
+ },
55
+ }),
56
+ defineTool({
57
+ name: 'proposal_accept',
58
+ title: 'Accept proposal',
59
+ description: 'Apply a proposal to the working tree (a person should have reviewed the diff first).',
60
+ shape: { id: z.string(), reviewedBy: z.string().optional() },
61
+ access: 'write',
62
+ domain: 'agents',
63
+ capability: 'agents',
64
+ annotations: { destructiveHint: true },
65
+ handler: async (args, ctx) => {
66
+ const written = new ProposalStore(ctx.rootDir).accept(args.id, {
67
+ reviewedBy: args.reviewedBy ?? ctx.principal.name,
68
+ });
69
+ return {
70
+ text: `Applied ${written.length} file(s):\n${written.map((w) => `- ${w}`).join('\n')}\nRun feature_lint next.`,
71
+ data: { id: args.id, written },
72
+ };
73
+ },
74
+ }),
75
+ defineTool({
76
+ name: 'proposal_reject',
77
+ title: 'Reject proposal',
78
+ description: 'Mark a proposal rejected with an optional reason.',
79
+ shape: { id: z.string(), reason: z.string().optional() },
80
+ access: 'write',
81
+ domain: 'agents',
82
+ capability: 'agents',
83
+ handler: async (args, ctx) => {
84
+ const m = new ProposalStore(ctx.rootDir).reject(args.id, args.reason, ctx.principal.name);
85
+ return { text: `Rejected ${m.id}.`, data: m };
86
+ },
87
+ }),
88
+ ];
89
+ //# sourceMappingURL=proposal.js.map
@@ -0,0 +1,49 @@
1
+ import { z } from 'zod';
2
+ export declare const runTools: (import("../registry/registry.js").SdodsTool<{
3
+ project: z.ZodString;
4
+ env: z.ZodOptional<z.ZodString>;
5
+ tags: z.ZodOptional<z.ZodString>;
6
+ layers: z.ZodOptional<z.ZodArray<z.ZodEnum<{
7
+ api: "api";
8
+ ui: "ui";
9
+ hybrid: "hybrid";
10
+ recorded: "recorded";
11
+ }>>>;
12
+ browsers: z.ZodOptional<z.ZodArray<z.ZodEnum<{
13
+ chromium: "chromium";
14
+ firefox: "firefox";
15
+ webkit: "webkit";
16
+ "mobile-chrome": "mobile-chrome";
17
+ "mobile-safari": "mobile-safari";
18
+ }>>>;
19
+ process: z.ZodOptional<z.ZodString>;
20
+ modules: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
+ harMode: z.ZodOptional<z.ZodEnum<{
22
+ off: "off";
23
+ update: "update";
24
+ replay: "replay";
25
+ }>>;
26
+ workers: z.ZodOptional<z.ZodNumber>;
27
+ retries: z.ZodOptional<z.ZodNumber>;
28
+ grep: z.ZodOptional<z.ZodString>;
29
+ headed: z.ZodOptional<z.ZodBoolean>;
30
+ strict: z.ZodOptional<z.ZodBoolean>;
31
+ runId: z.ZodOptional<z.ZodString>;
32
+ }> | import("../registry/registry.js").SdodsTool<{
33
+ limit: z.ZodOptional<z.ZodNumber>;
34
+ project: z.ZodOptional<z.ZodString>;
35
+ }> | import("../registry/registry.js").SdodsTool<{
36
+ runId: z.ZodString;
37
+ }> | import("../registry/registry.js").SdodsTool<{
38
+ runId: z.ZodString;
39
+ fingerprint: z.ZodString;
40
+ retry: z.ZodOptional<z.ZodNumber>;
41
+ }> | import("../registry/registry.js").SdodsTool<{
42
+ runId: z.ZodOptional<z.ZodString>;
43
+ project: z.ZodOptional<z.ZodString>;
44
+ }> | import("../registry/registry.js").SdodsTool<{
45
+ project: z.ZodOptional<z.ZodString>;
46
+ runs: z.ZodOptional<z.ZodNumber>;
47
+ }>)[];
48
+ export declare function readRunLog(dir: string): string | undefined;
49
+ //# sourceMappingURL=run.d.ts.map