@haystackeditor/cli 0.15.22 → 0.15.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -105,6 +105,17 @@ haystack verify hosted status cv_<48-lowercase-hex-characters> \
105
105
  --json
106
106
  ```
107
107
 
108
+ Agents can use the same production lifecycle over stdio MCP:
109
+
110
+ ```bash
111
+ haystack verify hosted mcp --account <login>
112
+ ```
113
+
114
+ The hosted server exposes only `verify_start`, `verify_status`, and
115
+ `verify_wait`. Tool results use the same `cloud-verifier` schema as the CLI;
116
+ credentials, repository source, provider metadata, and sandbox mutation are
117
+ not exposed.
118
+
108
119
  **`haystack setup --json`** speaks NDJSON: events (`question`, `permission`,
109
120
  `progress`, `result`) on stdout, replies on stdin keyed by
110
121
  `requestID`/`permissionID`. `haystack schema setup` documents the full
@@ -0,0 +1,12 @@
1
+ import { type Tool } from '@modelcontextprotocol/sdk/types.js';
2
+ export declare const HOSTED_VERIFY_TOOLS: Tool[];
3
+ export interface HostedVerifyMcpOptions {
4
+ account?: string;
5
+ }
6
+ interface HostedToolContext {
7
+ token: string;
8
+ account?: string;
9
+ }
10
+ export declare function executeHostedVerifyTool(name: string, input: Record<string, unknown>, context: HostedToolContext): Promise<Record<string, unknown>>;
11
+ export declare function runHostedVerifyMcpServer(options: HostedVerifyMcpOptions): Promise<void>;
12
+ export {};
@@ -0,0 +1,234 @@
1
+ // Production Cloud Verifier MCP server: `haystack verify hosted mcp`.
2
+ //
3
+ // This intentionally exposes only the durable hosted run lifecycle. The local
4
+ // specimen MCP server has artifact and retained-sandbox tools whose data model
5
+ // does not exist in the source-free production result contract.
6
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
7
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
9
+ import { resolveAuthContext } from '../utils/auth.js';
10
+ import { buildHostedJson, getHostedVerification, hostedResumeCommand, parseHostedRepository, startHostedVerification, waitForHostedVerification, } from './verify-hosted.js';
11
+ const TERMINAL_STATUSES = new Set([
12
+ 'complete',
13
+ 'errored',
14
+ 'expired',
15
+ 'terminated',
16
+ ]);
17
+ const DEFAULT_TIMEOUT_SECONDS = 30 * 60;
18
+ const MAX_TIMEOUT_SECONDS = 60 * 60;
19
+ const accountInput = {
20
+ type: 'string',
21
+ description: 'Saved Haystack account login. Omit to use the server default or active account.',
22
+ };
23
+ const runIdInput = {
24
+ type: 'string',
25
+ pattern: '^cv_[0-9a-f]{48}$',
26
+ description: 'Owned hosted run id.',
27
+ };
28
+ export const HOSTED_VERIFY_TOOLS = [
29
+ {
30
+ name: 'verify_start',
31
+ description: 'Start an authenticated production verification for an exact GitHub commit pair. Returns immediately; call verify_wait next.',
32
+ inputSchema: {
33
+ type: 'object',
34
+ additionalProperties: false,
35
+ properties: {
36
+ repository: {
37
+ type: 'string',
38
+ pattern: '^[A-Za-z0-9_.-]{1,100}/[A-Za-z0-9_.-]{1,100}$',
39
+ description: 'Exact GitHub owner/repository name.',
40
+ },
41
+ base_commit: {
42
+ type: 'string',
43
+ pattern: '^[0-9a-f]{40}$',
44
+ description: 'Exact 40-character lowercase base commit SHA.',
45
+ },
46
+ head_commit: {
47
+ type: 'string',
48
+ pattern: '^[0-9a-f]{40}$',
49
+ description: 'Exact 40-character lowercase head commit SHA.',
50
+ },
51
+ idempotency_key: {
52
+ type: 'string',
53
+ pattern: '^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$',
54
+ description: 'Optional stable retry key. Omit to derive it from the exact repository and commits.',
55
+ },
56
+ account: accountInput,
57
+ },
58
+ required: ['repository', 'base_commit', 'head_commit'],
59
+ },
60
+ },
61
+ {
62
+ name: 'verify_status',
63
+ description: 'Read the current source-free production receipt for an owned hosted run.',
64
+ inputSchema: {
65
+ type: 'object',
66
+ additionalProperties: false,
67
+ properties: {
68
+ run_id: runIdInput,
69
+ account: accountInput,
70
+ },
71
+ required: ['run_id'],
72
+ },
73
+ },
74
+ {
75
+ name: 'verify_wait',
76
+ description: 'Wait for an owned production run to finish. A timeout returns the last observed status with timed_out=true; the run keeps going.',
77
+ inputSchema: {
78
+ type: 'object',
79
+ additionalProperties: false,
80
+ properties: {
81
+ run_id: runIdInput,
82
+ timeout_seconds: {
83
+ type: 'integer',
84
+ minimum: 10,
85
+ maximum: MAX_TIMEOUT_SECONDS,
86
+ description: 'Bounded wait in seconds (default 1800, maximum 3600).',
87
+ },
88
+ account: accountInput,
89
+ },
90
+ required: ['run_id'],
91
+ },
92
+ },
93
+ ];
94
+ function requireInputString(input, field) {
95
+ const value = input[field];
96
+ if (typeof value !== 'string' || value.length === 0) {
97
+ throw new Error(`${field} required`);
98
+ }
99
+ return value;
100
+ }
101
+ function optionalInputString(input, field) {
102
+ const value = input[field];
103
+ if (value === undefined)
104
+ return undefined;
105
+ if (typeof value !== 'string' || value.length === 0) {
106
+ throw new Error(`${field} must be a non-empty string`);
107
+ }
108
+ return value;
109
+ }
110
+ function assertExactInputFields(input, allowed) {
111
+ const allowedFields = new Set(allowed);
112
+ const unsupported = Object.keys(input).find(field => !allowedFields.has(field));
113
+ if (unsupported)
114
+ throw new Error(`unsupported input field: ${unsupported}`);
115
+ }
116
+ function assertToolInputFields(name, input) {
117
+ if (name === 'verify_start') {
118
+ assertExactInputFields(input, [
119
+ 'account',
120
+ 'base_commit',
121
+ 'head_commit',
122
+ 'idempotency_key',
123
+ 'repository',
124
+ ]);
125
+ return;
126
+ }
127
+ if (name === 'verify_status') {
128
+ assertExactInputFields(input, ['account', 'run_id']);
129
+ return;
130
+ }
131
+ if (name === 'verify_wait') {
132
+ assertExactInputFields(input, ['account', 'run_id', 'timeout_seconds']);
133
+ return;
134
+ }
135
+ throw new Error(`unknown tool: ${name}`);
136
+ }
137
+ function nextCommand(state, account) {
138
+ return TERMINAL_STATUSES.has(state.status)
139
+ ? undefined
140
+ : hostedResumeCommand(state.runId, account);
141
+ }
142
+ export async function executeHostedVerifyTool(name, input, context) {
143
+ assertToolInputFields(name, input);
144
+ if (name === 'verify_start') {
145
+ const repository = requireInputString(input, 'repository');
146
+ const started = await startHostedVerification(repository, {
147
+ base: requireInputString(input, 'base_commit'),
148
+ head: requireInputString(input, 'head_commit'),
149
+ idempotencyKey: optionalInputString(input, 'idempotency_key'),
150
+ wait: false,
151
+ }, context.token);
152
+ return buildHostedJson('start', started.state, {
153
+ account: context.account,
154
+ idempotencyKey: started.idempotencyKey,
155
+ nextCommand: nextCommand(started.state, context.account),
156
+ });
157
+ }
158
+ if (name === 'verify_status') {
159
+ const state = await getHostedVerification(requireInputString(input, 'run_id'), context.token);
160
+ return buildHostedJson('status', state, {
161
+ account: context.account,
162
+ nextCommand: nextCommand(state, context.account),
163
+ });
164
+ }
165
+ if (name === 'verify_wait') {
166
+ const runId = requireInputString(input, 'run_id');
167
+ const rawTimeout = input.timeout_seconds;
168
+ const timeoutSeconds = rawTimeout === undefined
169
+ ? DEFAULT_TIMEOUT_SECONDS
170
+ : rawTimeout;
171
+ if (typeof timeoutSeconds !== 'number'
172
+ || !Number.isInteger(timeoutSeconds)
173
+ || timeoutSeconds < 10
174
+ || timeoutSeconds > MAX_TIMEOUT_SECONDS) {
175
+ throw new Error('timeout_seconds must be an integer from 10 to 3600');
176
+ }
177
+ const waited = await waitForHostedVerification(runId, context.token, {
178
+ intervalMs: 3_000,
179
+ timeoutMs: timeoutSeconds * 1_000,
180
+ });
181
+ return buildHostedJson('status', waited.state, {
182
+ account: context.account,
183
+ nextCommand: nextCommand(waited.state, context.account),
184
+ timedOut: waited.timedOut,
185
+ });
186
+ }
187
+ throw new Error(`unknown tool: ${name}`);
188
+ }
189
+ async function resolveToolContext(name, input, options) {
190
+ const requestedAccount = optionalInputString(input, 'account') ?? options.account;
191
+ const repository = name === 'verify_start'
192
+ ? parseHostedRepository(requireInputString(input, 'repository'))
193
+ : undefined;
194
+ const auth = await resolveAuthContext({
195
+ preferredLogin: requestedAccount,
196
+ owner: repository?.owner,
197
+ repo: repository?.repository,
198
+ });
199
+ return {
200
+ token: auth.token,
201
+ account: auth.token.startsWith('hsk_live_') ? undefined : auth.login,
202
+ };
203
+ }
204
+ export async function runHostedVerifyMcpServer(options) {
205
+ const mcp = new Server({ name: 'haystack-verify-hosted', version: '1.0.0' }, { capabilities: { tools: {} } });
206
+ mcp.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: HOSTED_VERIFY_TOOLS }));
207
+ mcp.setRequestHandler(CallToolRequestSchema, async (request) => {
208
+ const { name, arguments: args } = request.params;
209
+ const input = (args ?? {});
210
+ try {
211
+ // Reject undeclared fields before account resolution so callers cannot
212
+ // use an invalid request to trigger credential or repository lookup.
213
+ assertToolInputFields(name, input);
214
+ const context = await resolveToolContext(name, input, options);
215
+ const result = await executeHostedVerifyTool(name, input, context);
216
+ return {
217
+ content: [{
218
+ type: 'text',
219
+ text: JSON.stringify(result, null, 2),
220
+ }],
221
+ };
222
+ }
223
+ catch (error) {
224
+ return {
225
+ isError: true,
226
+ content: [{
227
+ type: 'text',
228
+ text: error instanceof Error ? error.message : String(error),
229
+ }],
230
+ };
231
+ }
232
+ });
233
+ await mcp.connect(new StdioServerTransport());
234
+ }
package/dist/index.js CHANGED
@@ -256,6 +256,26 @@ Example:
256
256
  const { verifyHostedStatusCommand } = await import('./commands/verify-hosted.js');
257
257
  return runPublicCommand(() => verifyHostedStatusCommand(runId, options), options.json);
258
258
  });
259
+ hostedVerify
260
+ .command('mcp')
261
+ .description('Run a stdio MCP server for the production verifier lifecycle')
262
+ .option('--account <login>', 'Default saved Haystack account for status and wait tools')
263
+ .addHelpText('after', `
264
+ Tools: verify_start, verify_status, verify_wait.
265
+ No credential, repository source, provider metadata, or sandbox mutation tool
266
+ is exposed through this production server.
267
+ `)
268
+ .action(async (_options, cmd) => {
269
+ const options = { ...cmd.optsWithGlobals(), ...cmd.opts() };
270
+ try {
271
+ const { runHostedVerifyMcpServer } = await import('./commands/verify-hosted-mcp.js');
272
+ await runHostedVerifyMcpServer(options);
273
+ }
274
+ catch (err) {
275
+ console.error(chalk.red('hosted verify mcp server failed:'), err instanceof Error ? err.message : err);
276
+ process.exit(1);
277
+ }
278
+ });
259
279
  verify
260
280
  .command('reproducibility')
261
281
  .description('Separate frozen-plan drift from executor/judgment nondeterminism')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haystackeditor/cli",
3
- "version": "0.15.22",
3
+ "version": "0.15.23",
4
4
  "description": "Set up Haystack for your project — automated PR review, triage, and merge queue",
5
5
  "type": "module",
6
6
  "bin": {