@seamward/setup-mcp 0.1.0-alpha.9

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/dist/server.js ADDED
@@ -0,0 +1,1959 @@
1
+ import { createHash } from "node:crypto";
2
+ import { readFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { McpServer } from "@modelcontextprotocol/server";
5
+ import { activatePlanContractVersion, applyLocalSetupBatch, checkFirstSetupObservation, createLocalSetupEngine, discoverIntegrationGroups, isAutomaticIntegrationGroup, inspectSetupIntegrationBinding, LocalSetupEngineError, previewPlanContract, previewPlanContractActivation, readContractStatus, readMatchingActivePlanContract, readSetupPlan, readSetupPlans, recordSetupIntegrationBinding, reconcileActivePlanContract, registeredContractVersionIdForFile, registerPlanContracts, SeamwardRemoteApiError, } from "@seamward/cli";
6
+ import * as z from "zod/v4";
7
+ import { setupMcpInputSchemas, setupMcpErrorOutputSchema, setupMcpErrorRecovery, setupMcpSchemaVersion, setupMcpOutputSchemas, setupMcpToolDefinitions, legacySetupMcpToolDefinitions, toolAnnotations, } from "./contracts.js";
8
+ import { setupMcpVersion } from "./version.js";
9
+ const integrationEffectSchema = z
10
+ .object({
11
+ action: z.enum(["create", "reuse"]),
12
+ direction: z.enum(["inbound", "outbound"]),
13
+ integrationId: z.string().startsWith("int_"),
14
+ name: z.string().min(1).max(120),
15
+ protocol: z.enum(["http-api", "http-webhook", "queue", "scheduled-feed"]),
16
+ provider: z.string().min(1).max(120),
17
+ setupId: z.string().min(1).max(128),
18
+ })
19
+ .strict();
20
+ const integrationPreviewResponseSchema = z
21
+ .object({
22
+ effects: z.array(integrationEffectSchema).min(1).max(20),
23
+ expiresAt: z.string().datetime(),
24
+ fingerprint: z.string().regex(/^sha256:[a-f0-9]{64}$/),
25
+ operationId: z.string().startsWith("setupop_"),
26
+ })
27
+ .strict();
28
+ const provisionedIntegrationSchema = z
29
+ .object({
30
+ connectionKey: z.string().min(1),
31
+ direction: z.enum(["inbound", "outbound"]),
32
+ environmentId: z.string().min(1),
33
+ id: z.string().startsWith("int_"),
34
+ integrationKey: z.string().startsWith("sw_int_"),
35
+ name: z.string().min(1),
36
+ protocol: z.enum(["http-api", "http-webhook", "queue", "scheduled-feed"]),
37
+ provider: z.string().min(1),
38
+ setupId: z.string().min(1),
39
+ sourceKey: z.string().startsWith("sw_src_"),
40
+ })
41
+ .strict();
42
+ const integrationApplyResponseSchema = z
43
+ .object({
44
+ integrations: z.array(provisionedIntegrationSchema).min(1).max(20),
45
+ operationId: z.string().startsWith("setupop_"),
46
+ status: z.literal("completed"),
47
+ })
48
+ .strict();
49
+ function result(name, data) {
50
+ const parsed = setupMcpOutputSchemas[name].safeParse(JSON.parse(JSON.stringify({ schemaVersion: setupMcpSchemaVersion, data })));
51
+ if (!parsed.success)
52
+ throw new Error("Invalid local setup tool output");
53
+ const structuredContent = parsed.data;
54
+ return {
55
+ content: [
56
+ { type: "text", text: JSON.stringify(structuredContent) },
57
+ ],
58
+ structuredContent,
59
+ };
60
+ }
61
+ class SetupMcpOperationError extends Error {
62
+ code;
63
+ constructor(code) {
64
+ super(code);
65
+ this.code = code;
66
+ this.name = "SetupMcpOperationError";
67
+ }
68
+ }
69
+ function errorCode(error) {
70
+ if (error instanceof SetupMcpOperationError)
71
+ return error.code;
72
+ if (error instanceof LocalSetupEngineError)
73
+ return error.code;
74
+ if (error instanceof SeamwardRemoteApiError)
75
+ return error.code;
76
+ if (error instanceof z.ZodError)
77
+ return "invalid_input";
78
+ const message = error instanceof Error ? error.message : "";
79
+ if (/stale setup lock requires manual recovery/i.test(message))
80
+ return "stale_setup_lock";
81
+ if (/generated setup files are being updated/i.test(message))
82
+ return "setup_state_conflict";
83
+ if (/scan .*limit exceeded/i.test(message))
84
+ return "scan_limit_exceeded";
85
+ if (/state (?:fingerprint does not match|is being updated)/i.test(message))
86
+ return "setup_state_conflict";
87
+ if (/state changed after contract activation preview/i.test(message))
88
+ return "setup_state_conflict";
89
+ if (/source verification required/i.test(message))
90
+ return "source_verification_required";
91
+ if (/contract document changed after the setup plan was reviewed/i.test(message))
92
+ return "plan_conflict";
93
+ if (/contract target|activation plan fingerprint/i.test(message))
94
+ return "plan_conflict";
95
+ if (/refusing to overwrite|generated file changed/i.test(message))
96
+ return "generated_file_conflict";
97
+ if (/activation failed.*(?:409|active_contract_conflict)/i.test(message))
98
+ return "contract_activation_conflict";
99
+ if (/registration failed|activation failed|status failed/i.test(message))
100
+ return "remote_request_failed";
101
+ if (/returned an invalid response/i.test(message))
102
+ return "remote_response_invalid";
103
+ return "internal_error";
104
+ }
105
+ function errorResult(error) {
106
+ const code = errorCode(error);
107
+ const messages = {
108
+ plan_not_found: "The requested setup plan is unavailable. Analyze and plan again.",
109
+ legacy_plan_requires_replan: "This setup plan predates source-backed evidence. Analyze and plan again.",
110
+ confirmation_required: "The exact confirmation value is required.",
111
+ no_matching_actions: "No planned instrumentation matches those files.",
112
+ invalid_input: "The tool input does not match the published schema.",
113
+ scan_limit_exceeded: "Repository analysis exceeded a configured safety limit.",
114
+ generated_file_conflict: "A generated file changed and will not be overwritten.",
115
+ apply_in_progress: "Another local setup apply is already in progress. Retry after it finishes.",
116
+ setup_preview_stale: "The reviewed Integration preview is stale. Review the current setup again.",
117
+ idempotency_conflict: "That idempotency key was already used for a different setup request.",
118
+ verification_failed: "The reviewed local setup did not pass deterministic project verification.",
119
+ setup_state_conflict: "Setup evidence changed. Verify again before recording evidence.",
120
+ stale_setup_lock: "A setup lock remains after its owner stopped. Confirm the recorded process is absent, then remove only the named lock file.",
121
+ configuration_required: "The application environment setup authorization is not configured.",
122
+ contract_preview_expired: "The approval preview is unavailable. Preview the operation again.",
123
+ contract_activation_conflict: "The active contract changed after preview. Read status and preview again.",
124
+ authentication_required: "The Seamward setup authorization is invalid, expired, or revoked.",
125
+ insufficient_scope: "The Seamward setup authorization does not grant this operation.",
126
+ integration_scope_mismatch: "The contract operations do not match this Integration scope.",
127
+ integration_mapping_required: "The discovered boundary cannot be matched to exactly one existing Integration in the selected application environment.",
128
+ contract_version_conflict: "That contract version conflicts with an existing immutable version.",
129
+ source_verification_required: "Verify and record the planned source changes before contract activation.",
130
+ not_found: "The requested Seamward resource was not found in this workspace.",
131
+ remote_conflict: "The remote operation conflicts with current Seamward state.",
132
+ rate_limited: "The Seamward API rate limit was reached. Retry after a delay.",
133
+ service_unavailable: "The Seamward API is temporarily unavailable. Retry later.",
134
+ remote_request_failed: "The Seamward API request failed. Verify access and retry.",
135
+ remote_response_invalid: "The Seamward API returned an invalid response.",
136
+ plan_conflict: "The saved plan no longer matches the requested action.",
137
+ internal_error: "The local setup operation failed.",
138
+ };
139
+ const structuredContent = setupMcpErrorOutputSchema.parse({
140
+ schemaVersion: setupMcpSchemaVersion,
141
+ error: {
142
+ code,
143
+ retryable: setupMcpErrorRecovery[code].retryable,
144
+ message: messages[code] ?? messages.internal_error,
145
+ recovery: setupMcpErrorRecovery[code].recovery,
146
+ },
147
+ });
148
+ return {
149
+ isError: true,
150
+ content: [
151
+ { type: "text", text: JSON.stringify(structuredContent) },
152
+ ],
153
+ structuredContent,
154
+ };
155
+ }
156
+ export function setupApiErrorCode(status, body) {
157
+ const remoteCode = body && typeof body === "object" && !Array.isArray(body)
158
+ ? body.error
159
+ : undefined;
160
+ if (status === 401)
161
+ return "authentication_required";
162
+ if (status === 403)
163
+ return "insufficient_scope";
164
+ if (status === 404)
165
+ return "not_found";
166
+ if (status === 409) {
167
+ if (remoteCode === "setup_preview_stale" ||
168
+ remoteCode === "idempotency_conflict" ||
169
+ remoteCode === "apply_in_progress" ||
170
+ remoteCode === "integration_mapping_required" ||
171
+ remoteCode === "integration_scope_mismatch") {
172
+ return remoteCode;
173
+ }
174
+ return "remote_conflict";
175
+ }
176
+ if (status === 429)
177
+ return "rate_limited";
178
+ if (status >= 500)
179
+ return "service_unavailable";
180
+ return "remote_request_failed";
181
+ }
182
+ function definition(name) {
183
+ const found = [
184
+ ...setupMcpToolDefinitions,
185
+ ...legacySetupMcpToolDefinitions,
186
+ ].find((tool) => tool.name === name);
187
+ if (!found)
188
+ throw new Error("Unknown local setup MCP tool");
189
+ return found;
190
+ }
191
+ function config(name, inputSchema) {
192
+ const tool = definition(name);
193
+ return {
194
+ title: tool.title,
195
+ description: tool.description,
196
+ inputSchema,
197
+ outputSchema: tool.outputSchema,
198
+ annotations: toolAnnotations(tool.readOnly, tool.destructive, tool.idempotent, tool.openWorld),
199
+ };
200
+ }
201
+ function approvalFingerprint(value) {
202
+ return `sha256:${createHash("sha256").update(JSON.stringify(value)).digest("hex")}`;
203
+ }
204
+ const approvalPreviewTtlMs = 5 * 60 * 1000;
205
+ const maximumApprovalPreviews = 100;
206
+ function storeApprovalPreview(previews, fingerprint, value) {
207
+ const now = Date.now();
208
+ for (const [key, preview] of previews) {
209
+ if (preview.expiresAt <= now)
210
+ previews.delete(key);
211
+ }
212
+ while (previews.size >= maximumApprovalPreviews) {
213
+ const oldest = previews.keys().next().value;
214
+ if (!oldest)
215
+ break;
216
+ previews.delete(oldest);
217
+ }
218
+ previews.set(fingerprint, { value, expiresAt: now + approvalPreviewTtlMs });
219
+ }
220
+ function readApprovalPreview(previews, fingerprint) {
221
+ const preview = previews.get(fingerprint);
222
+ if (!preview)
223
+ return undefined;
224
+ if (preview.expiresAt <= Date.now()) {
225
+ previews.delete(fingerprint);
226
+ return undefined;
227
+ }
228
+ return preview.value;
229
+ }
230
+ export async function createSeamwardSetupMcpServer({ root, engine: suppliedEngine, credentialProvider, env = (name) => process.env[name], fetchFn = fetch, legacyTools = false, }) {
231
+ const engine = suppliedEngine ?? (await createLocalSetupEngine({ root }));
232
+ const server = new McpServer({ name: "seamward-setup", version: setupMcpVersion }, {
233
+ instructions: "When the user asks to set up Seamward, analyze the configured backend service and infer its supported HTTP API, webhook, queue, and scheduled-feed boundaries and contract candidates. Do not assume a provider, protocol, operation, or contract. If one setup is unambiguous, prepare it automatically. If several are valid, present concise plain-language choices. Preview all local effects before one local change approval, run repository verification as part of the approved apply, preview any remote contract effect before one remote connection approval, then check status read-only. Keep file selection mechanics and protocol identifiers internal unless explaining a genuine ambiguity. Never ask the user to copy internal values. Never read or edit environment files.",
234
+ });
235
+ const contractPreviews = new Map();
236
+ const activationPreviews = new Map();
237
+ let activePlan = null;
238
+ let activeSetups = [];
239
+ let retiredSetups = [];
240
+ let localRemovalOnlyComplete = false;
241
+ let reviewedIntegrationMappings = new Map();
242
+ let pendingRemote = null;
243
+ let pendingRemoteSet = [];
244
+ let pendingProvisioning = null;
245
+ const currentPlan = async () => activePlan ?? readSetupPlan(root);
246
+ const setupIdForPlan = (plan) => plan.setupId ??
247
+ `default-${createHash("sha256")
248
+ .update(plan.fingerprint)
249
+ .digest("hex")
250
+ .slice(0, 16)}`;
251
+ const requestSetupApi = async ({ apiKey, body, endpoint, idempotencyKey, pathname, schema, }) => {
252
+ const base = (endpoint ?? "https://api.seamward.com").replace(/\/$/, "");
253
+ const response = await fetchFn(`${base}${pathname}`, {
254
+ body: JSON.stringify(body),
255
+ headers: {
256
+ authorization: `Bearer ${apiKey}`,
257
+ "content-type": "application/json",
258
+ ...(idempotencyKey ? { "idempotency-key": idempotencyKey } : {}),
259
+ },
260
+ method: "POST",
261
+ });
262
+ const responseBody = await response.json().catch(() => null);
263
+ if (!response.ok) {
264
+ throw new SetupMcpOperationError(setupApiErrorCode(response.status, responseBody));
265
+ }
266
+ const parsed = schema.safeParse(responseBody);
267
+ if (!parsed.success)
268
+ throw new SetupMcpOperationError("remote_response_invalid");
269
+ return parsed.data;
270
+ };
271
+ const credentialsForPlan = async (plan, setupName) => {
272
+ if (credentialProvider) {
273
+ const credential = await credentialProvider();
274
+ if (!credential.integrations || credential.integrations.length === 0) {
275
+ if (!credential.connectionKey) {
276
+ throw new SetupMcpOperationError("integration_mapping_required");
277
+ }
278
+ return { ...credential, connectionKey: credential.connectionKey };
279
+ }
280
+ const scopeMatches = credential.integrations.filter(({ direction, protocol }) => direction === plan.integrationScope?.direction &&
281
+ protocol === plan.integrationScope.protocol);
282
+ const reviewedIntegrationId = plan.setupId
283
+ ? (reviewedIntegrationMappings.get(plan.setupId) ??
284
+ (await inspectSetupIntegrationBinding(plan, root)) ??
285
+ undefined)
286
+ : undefined;
287
+ if (reviewedIntegrationId) {
288
+ const reviewed = scopeMatches.find(({ id }) => id === reviewedIntegrationId);
289
+ if (!reviewed) {
290
+ throw new SetupMcpOperationError("integration_mapping_required");
291
+ }
292
+ return {
293
+ ...credential,
294
+ connectionKey: reviewed.connectionKey,
295
+ integrationId: reviewed.id,
296
+ };
297
+ }
298
+ const tokens = (setupName ?? plan.setupId ?? "")
299
+ .toLowerCase()
300
+ .split(/[^a-z0-9]+/)
301
+ .filter((token) => token.length > 2 &&
302
+ !["api", "http", "webhook", "webhooks", "integration"].includes(token));
303
+ const ranked = scopeMatches
304
+ .map((integration) => ({
305
+ integration,
306
+ score: tokens.filter((token) => `${integration.name} ${integration.provider}`
307
+ .toLowerCase()
308
+ .includes(token)).length,
309
+ }))
310
+ .sort((left, right) => right.score - left.score);
311
+ const best = ranked[0];
312
+ const tied = ranked[1]?.score === best?.score;
313
+ if (!best || tied) {
314
+ throw new SetupMcpOperationError("integration_mapping_required");
315
+ }
316
+ return {
317
+ ...credential,
318
+ connectionKey: best.integration.connectionKey,
319
+ integrationId: best.integration.id,
320
+ };
321
+ }
322
+ const connectionKey = env(plan.connectionKeyEnvironment);
323
+ const apiKey = env("SEAMWARD_API_KEY");
324
+ if (!connectionKey || !apiKey) {
325
+ throw new SetupMcpOperationError("configuration_required");
326
+ }
327
+ return {
328
+ connectionKey,
329
+ apiKey,
330
+ endpoint: env("SEAMWARD_MANAGEMENT_API_URL"),
331
+ };
332
+ };
333
+ const integrationCandidatesForPlan = async (plan) => {
334
+ if (!credentialProvider)
335
+ return [];
336
+ const credential = await credentialProvider();
337
+ return (credential.integrations ?? [])
338
+ .filter(({ direction, protocol }) => direction === plan.integrationScope?.direction &&
339
+ protocol === plan.integrationScope.protocol)
340
+ .map(({ id, name, provider, direction, protocol }) => ({
341
+ integrationId: id,
342
+ name,
343
+ provider,
344
+ direction,
345
+ protocol,
346
+ }));
347
+ };
348
+ const deriveDeclaredVersion = async (file) => {
349
+ const content = await readFile(path.join(root, file), "utf8");
350
+ try {
351
+ const document = JSON.parse(content);
352
+ if (typeof document.info?.version === "string" &&
353
+ document.info.version.trim()) {
354
+ return document.info.version.trim();
355
+ }
356
+ if (typeof document.$id === "string" && document.$id.trim()) {
357
+ return document.$id.trim().slice(0, 128);
358
+ }
359
+ }
360
+ catch {
361
+ const yamlVersion = content.match(/^\s*version:\s*["']?([^\s"']+)/m)?.[1];
362
+ if (yamlVersion)
363
+ return yamlVersion.slice(0, 128);
364
+ }
365
+ const packageDocument = JSON.parse(await readFile(path.join(root, "package.json"), "utf8"));
366
+ return typeof packageDocument.version === "string" &&
367
+ packageDocument.version.trim()
368
+ ? packageDocument.version.trim().slice(0, 128)
369
+ : "1.0.0";
370
+ };
371
+ const suggestedSetupIdentity = async ({ name, plan, }) => {
372
+ const contractFile = plan.actions.find(({ type, file }) => type === "register_contract" && file)?.file;
373
+ let suggestedName = name;
374
+ if (contractFile) {
375
+ try {
376
+ const document = JSON.parse(await readFile(path.join(root, contractFile), "utf8"));
377
+ if (typeof document.info?.title === "string" &&
378
+ document.info.title.trim()) {
379
+ suggestedName = document.info.title
380
+ .trim()
381
+ .replace(/\s+(?:api|provider|webhooks?)$/i, "");
382
+ }
383
+ }
384
+ catch {
385
+ // Invalid contract content is reported by the contract preview later.
386
+ }
387
+ }
388
+ let provider = suggestedName;
389
+ const outboundTarget = plan.actions.find(({ direction, targetHint }) => direction === "outbound" && targetHint)?.targetHint;
390
+ if (outboundTarget) {
391
+ try {
392
+ const hostname = new URL(outboundTarget).hostname
393
+ .replace(/^www\./, "")
394
+ .split(".")[0];
395
+ if (hostname) {
396
+ provider = hostname
397
+ .split(/[-_]+/)
398
+ .filter(Boolean)
399
+ .map((part) => `${part[0]?.toUpperCase()}${part.slice(1)}`)
400
+ .join(" ");
401
+ }
402
+ }
403
+ catch {
404
+ // Non-URL target hints keep the contract-derived provider name.
405
+ }
406
+ }
407
+ return { name: suggestedName, provider };
408
+ };
409
+ const connectReviewedSetup = async (pending) => {
410
+ const setupEngine = activeSetups.find(({ plan }) => plan.setupId === pending.plan.setupId)
411
+ ?.engine ?? engine;
412
+ const verification = await setupEngine.verify({
413
+ planFingerprint: pending.plan.fingerprint,
414
+ });
415
+ if (!verification.sourceBackedComplete) {
416
+ throw new SetupMcpOperationError("source_verification_required");
417
+ }
418
+ if (verification.stateFingerprint !== pending.reviewedStateFingerprint) {
419
+ const reconciledContractVersionId = await registeredContractVersionIdForFile(pending.plan, root, pending.file);
420
+ const reconciledActivation = verification.remoteLifecycle.activation?.contractVersionId;
421
+ if (!reconciledContractVersionId ||
422
+ (reconciledActivation !== undefined &&
423
+ reconciledActivation !== reconciledContractVersionId)) {
424
+ throw new SetupMcpOperationError("setup_state_conflict");
425
+ }
426
+ pending.reviewedStateFingerprint = verification.stateFingerprint;
427
+ }
428
+ const credentials = await credentialsForPlan(pending.plan, pending.name);
429
+ if ("integrationId" in credentials &&
430
+ credentials.integrationId &&
431
+ credentials.integrationId !== pending.integrationId) {
432
+ throw new SetupMcpOperationError("setup_state_conflict");
433
+ }
434
+ if (pending.existingActiveContractVersionId) {
435
+ await reconcileActivePlanContract(pending.plan, root, {
436
+ apiKey: credentials.apiKey,
437
+ connectionKey: credentials.connectionKey,
438
+ contractVersionId: pending.existingActiveContractVersionId,
439
+ declaredVersion: pending.declaredVersion,
440
+ endpoint: credentials.endpoint,
441
+ file: pending.file,
442
+ fetchFn,
443
+ });
444
+ return {
445
+ status: "already_connected",
446
+ contractStatus: "active",
447
+ };
448
+ }
449
+ let contractVersionId = await registeredContractVersionIdForFile(pending.plan, root, pending.file);
450
+ if (!contractVersionId) {
451
+ const registered = await registerPlanContracts(pending.plan, root, {
452
+ apiKey: credentials.apiKey,
453
+ connectionKey: credentials.connectionKey,
454
+ declaredVersion: pending.declaredVersion,
455
+ file: pending.file,
456
+ endpoint: credentials.endpoint,
457
+ fetchFn,
458
+ writeCompletion: true,
459
+ });
460
+ contractVersionId = registered.registered[0]?.contractVersionId ?? null;
461
+ }
462
+ if (!contractVersionId) {
463
+ return {
464
+ status: "partially_connected",
465
+ contractStatus: "draft",
466
+ };
467
+ }
468
+ const status = await readContractStatus({
469
+ apiKey: credentials.apiKey,
470
+ connectionKey: credentials.connectionKey,
471
+ endpoint: credentials.endpoint,
472
+ fetchFn,
473
+ });
474
+ const activation = await previewPlanContractActivation(pending.plan, root, status, { contractVersionId, reason: "promote" });
475
+ await activatePlanContractVersion(pending.plan, root, activation, {
476
+ apiKey: credentials.apiKey,
477
+ connectionKey: credentials.connectionKey,
478
+ endpoint: credentials.endpoint,
479
+ fetchFn,
480
+ });
481
+ return {
482
+ status: activation.reconcile
483
+ ? "already_connected"
484
+ : "connected",
485
+ contractStatus: "active",
486
+ };
487
+ };
488
+ if (!legacyTools) {
489
+ const prepareInput = setupMcpInputSchemas.prepare_setup;
490
+ server.registerTool("prepare_setup", config("prepare_setup", prepareInput), async (input) => {
491
+ try {
492
+ const parsed = prepareInput.parse(input);
493
+ const analysis = await engine.analyze();
494
+ const groups = discoverIntegrationGroups(analysis.discovery);
495
+ const automaticGroups = groups.filter(isAutomaticIntegrationGroup);
496
+ const savedPlans = suppliedEngine ? [] : await readSetupPlans(root);
497
+ const automaticSetupIds = new Set(automaticGroups.map(({ id }) => id));
498
+ retiredSetups = savedPlans.filter(({ setupId }) => setupId && !automaticSetupIds.has(setupId));
499
+ localRemovalOnlyComplete = false;
500
+ const requestedSetupIds = new Set([
501
+ ...parsed.includeSetupIds,
502
+ ...parsed.excludeSetupIds,
503
+ ]);
504
+ if ([...requestedSetupIds].some((setupId) => !automaticGroups.some((group) => group.id === setupId))) {
505
+ throw new SetupMcpOperationError("invalid_input");
506
+ }
507
+ const selectedAutomaticGroups = automaticGroups.filter(({ id }) => (parsed.includeSetupIds.length === 0 ||
508
+ parsed.includeSetupIds.includes(id)) &&
509
+ !parsed.excludeSetupIds.includes(id));
510
+ if (automaticGroups.length > 0 &&
511
+ selectedAutomaticGroups.length === 0) {
512
+ throw new SetupMcpOperationError("no_matching_actions");
513
+ }
514
+ if (!suppliedEngine &&
515
+ !parsed.direction &&
516
+ (groups.length > 1 || savedPlans.length > 0) &&
517
+ (selectedAutomaticGroups.length > 0 || retiredSetups.length > 0)) {
518
+ const plannedSetups = await Promise.all(selectedAutomaticGroups.map(async (group) => {
519
+ const groupEngine = await createLocalSetupEngine({
520
+ root,
521
+ setupId: group.id,
522
+ });
523
+ const planned = await groupEngine.plan({
524
+ ...group.scope,
525
+ findingIds: group.findingIds,
526
+ contractFiles: group.contractFiles,
527
+ });
528
+ return {
529
+ engine: groupEngine,
530
+ name: group.name,
531
+ plan: planned.plan,
532
+ };
533
+ }));
534
+ const manual = plannedSetups.flatMap(({ name, plan }) => plan.actions.some(({ type, mode }) => type === "instrument_operation" && mode === "manual")
535
+ ? [`${name} requires a manual collector adapter.`]
536
+ : []);
537
+ if (manual.length > 0) {
538
+ return result("prepare_setup", {
539
+ status: "manual_required",
540
+ summary: "Seamward cannot safely automate every discovered integration boundary.",
541
+ reasons: manual,
542
+ });
543
+ }
544
+ activeSetups = plannedSetups;
545
+ activePlan = null;
546
+ pendingRemote = null;
547
+ pendingRemoteSet = [];
548
+ const removedGroups = retiredSetups.map((plan) => ({
549
+ setupId: plan.setupId,
550
+ files: [
551
+ ...plan.generatedFiles.map(({ path: file }) => file),
552
+ `.seamward/integrations/${plan.setupId}`,
553
+ ],
554
+ remoteAction: "left_active",
555
+ }));
556
+ return result("prepare_setup", {
557
+ status: "ready",
558
+ summary: `Seamward prepared ${selectedAutomaticGroups.length} current Integration binding${selectedAutomaticGroups.length === 1 ? "" : "s"} and ${retiredSetups.length} removed binding${retiredSetups.length === 1 ? "" : "s"} for review. Removed bindings are cleaned up locally; their remote Integrations remain active.`,
559
+ ...(selectedAutomaticGroups.length > 0
560
+ ? {
561
+ groups: selectedAutomaticGroups.map((group) => ({
562
+ setupId: group.id,
563
+ name: group.name,
564
+ scope: group.scope,
565
+ operationCount: group.findingIds.length,
566
+ contractFiles: group.contractFiles,
567
+ confidence: group.confidence,
568
+ requiredEnvironment: plannedSetups.find(({ plan }) => plan.setupId === group.id)?.plan.requiredEnvironment,
569
+ })),
570
+ }
571
+ : {}),
572
+ ...(removedGroups.length > 0 ? { removedGroups } : {}),
573
+ proposedChanges: [
574
+ ...plannedSetups.flatMap(({ name, plan }) => [
575
+ ...plan.generatedFiles.map(({ path: file }) => ({
576
+ file,
577
+ kind: "generated",
578
+ description: `Create the collector binding for ${name}.`,
579
+ })),
580
+ ...plan.actions.flatMap((action) => action.type === "instrument_operation" &&
581
+ action.file &&
582
+ action.line &&
583
+ action.direction &&
584
+ action.operationKind
585
+ ? [
586
+ {
587
+ file: action.file,
588
+ kind: "source",
589
+ operationLabel: `${name}: ${action.operationKind.replaceAll("_", " ")} at line ${action.line}`,
590
+ line: action.line,
591
+ direction: action.direction,
592
+ operationKind: action.operationKind,
593
+ adapter: action.adapter,
594
+ description: `Instrument this boundary for ${name} with ${action.adapter}.`,
595
+ },
596
+ ]
597
+ : []),
598
+ ...plan.actions.flatMap((action) => action.type === "register_contract" && action.file
599
+ ? [
600
+ {
601
+ file: action.file,
602
+ kind: "contract",
603
+ description: `Connect this contract to ${name}.`,
604
+ },
605
+ ]
606
+ : []),
607
+ ]),
608
+ ...removedGroups.flatMap(({ setupId, files }) => files.map((file) => ({
609
+ file,
610
+ kind: "removal",
611
+ description: `Remove local setup state for the deleted ${setupId} boundary. The remote Integration remains active.`,
612
+ }))),
613
+ ],
614
+ });
615
+ }
616
+ const scopes = analysis.integrationScopes;
617
+ if (scopes.length === 0) {
618
+ return result("prepare_setup", {
619
+ status: "manual_required",
620
+ summary: "No supported integration boundary was found in this service.",
621
+ reasons: [
622
+ "Add or identify a supported HTTP, webhook, queue, or scheduled-feed boundary.",
623
+ ],
624
+ });
625
+ }
626
+ if (!parsed.direction && scopes.length > 1) {
627
+ return result("prepare_setup", {
628
+ status: "needs_input",
629
+ summary: "Choose the integration boundary Seamward should configure.",
630
+ choices: scopes,
631
+ contractChoices: analysis.discovery.contracts.map(({ file }) => file),
632
+ });
633
+ }
634
+ const selectedScope = parsed.direction
635
+ ? { direction: parsed.direction, protocol: parsed.protocol }
636
+ : scopes[0];
637
+ const automaticScopeSupported = (selectedScope.direction === "outbound" &&
638
+ selectedScope.protocol === "http-api") ||
639
+ (selectedScope.direction === "inbound" &&
640
+ selectedScope.protocol === "http-webhook");
641
+ if (!automaticScopeSupported) {
642
+ return result("prepare_setup", {
643
+ status: "manual_required",
644
+ summary: "This integration boundary was discovered, but automatic local setup is not available for it.",
645
+ reasons: [
646
+ `${selectedScope.direction} ${selectedScope.protocol} automatic instrumentation is not shipped. Use the manual integration guide for this boundary.`,
647
+ ],
648
+ });
649
+ }
650
+ const discoveredContracts = analysis.discovery.contracts.map(({ file }) => file);
651
+ if (parsed.contractFiles.length > 1) {
652
+ return result("prepare_setup", {
653
+ status: "needs_input",
654
+ summary: "Choose one contract for this setup lifecycle. Connect additional contracts through separate reviewed setup runs.",
655
+ choices: [selectedScope],
656
+ contractChoices: parsed.contractFiles,
657
+ });
658
+ }
659
+ if (parsed.contractFiles.length === 0 &&
660
+ discoveredContracts.length > 1) {
661
+ return result("prepare_setup", {
662
+ status: "needs_input",
663
+ summary: "Choose the contract that belongs to this integration.",
664
+ choices: [selectedScope],
665
+ contractChoices: discoveredContracts,
666
+ });
667
+ }
668
+ const contractFiles = parsed.contractFiles.length > 0
669
+ ? parsed.contractFiles
670
+ : discoveredContracts.length === 1
671
+ ? discoveredContracts
672
+ : [];
673
+ const selectedContracts = analysis.discovery.contracts.filter(({ file }) => contractFiles.includes(file));
674
+ if (selectedContracts.some(({ format }) => format === "json_schema")) {
675
+ return result("prepare_setup", {
676
+ status: "manual_required",
677
+ summary: "The integration boundary is supported, but its JSON Schema contract needs a reviewed operation binding.",
678
+ reasons: [
679
+ "JSON Schema operation binding is not available in automatic setup. Use the manual contract setup guide.",
680
+ ],
681
+ });
682
+ }
683
+ const planned = await engine.plan({
684
+ ...selectedScope,
685
+ contractFiles,
686
+ });
687
+ const manualActions = planned.plan.actions.filter(({ type, mode }) => type === "instrument_operation" && mode === "manual");
688
+ const unsupportedAutomaticActions = planned.plan.actions.filter((action) => action.type === "instrument_operation" &&
689
+ action.mode !== "manual" &&
690
+ action.operationKind !== "http_client" &&
691
+ action.operationKind !== "webhook_handler");
692
+ if (planned.plan.install === null ||
693
+ manualActions.length > 0 ||
694
+ unsupportedAutomaticActions.length > 0) {
695
+ return result("prepare_setup", {
696
+ status: "manual_required",
697
+ summary: "This service shape is not supported for automatic local setup.",
698
+ reasons: unsupportedAutomaticActions.length
699
+ ? [
700
+ `${selectedScope.direction} ${selectedScope.protocol} automatic instrumentation is not shipped. Use the manual integration guide for this boundary.`,
701
+ ]
702
+ : manualActions.length
703
+ ? [
704
+ "One or more operations require a manually implemented collector adapter.",
705
+ ]
706
+ : [
707
+ "Automatic setup currently supports Node.js TypeScript and JavaScript services.",
708
+ ],
709
+ });
710
+ }
711
+ const proposedSourceActions = planned.plan.actions.filter((action) => action.type === "instrument_operation" &&
712
+ typeof action.file === "string" &&
713
+ typeof action.line === "number" &&
714
+ typeof action.direction === "string" &&
715
+ typeof action.operationKind === "string");
716
+ activePlan = planned.plan;
717
+ activeSetups = [];
718
+ pendingRemote = null;
719
+ pendingRemoteSet = [];
720
+ return result("prepare_setup", {
721
+ status: "ready",
722
+ summary: `Seamward found ${planned.plan.discovery.findings.length} operation${planned.plan.discovery.findings.length === 1 ? "" : "s"} for one ${selectedScope.direction} ${selectedScope.protocol} integration.`,
723
+ scope: selectedScope,
724
+ proposedChanges: [
725
+ ...planned.plan.generatedFiles.map(({ path: file }) => ({
726
+ file,
727
+ kind: "generated",
728
+ description: "Create the collector bootstrap.",
729
+ })),
730
+ ...proposedSourceActions.map((action) => {
731
+ const operationNumber = proposedSourceActions
732
+ .filter((candidate) => candidate.file === action.file)
733
+ .findIndex((candidate) => candidate.id === action.id) + 1;
734
+ const readableKind = action.operationKind.replaceAll("_", " ");
735
+ const operationLabel = `Operation ${operationNumber}: ${action.direction} ${readableKind} at line ${action.line}`;
736
+ return {
737
+ file: action.file,
738
+ kind: "source",
739
+ operationLabel,
740
+ line: action.line,
741
+ direction: action.direction,
742
+ operationKind: action.operationKind,
743
+ adapter: action.adapter,
744
+ description: `${operationLabel}. Wrap this boundary with ${action.adapter}.`,
745
+ };
746
+ }),
747
+ ...(planned.plan.install
748
+ ? [
749
+ {
750
+ file: "package.json",
751
+ kind: "dependency",
752
+ description: `Use ${planned.plan.install.package}.`,
753
+ },
754
+ ]
755
+ : []),
756
+ ...contractFiles.map((file) => ({
757
+ file,
758
+ kind: "contract",
759
+ description: "Register this reviewed contract during remote connection.",
760
+ })),
761
+ ],
762
+ });
763
+ }
764
+ catch (error) {
765
+ return errorResult(error);
766
+ }
767
+ });
768
+ const applyLocalInput = setupMcpInputSchemas.apply_local_setup;
769
+ server.registerTool("apply_local_setup", config("apply_local_setup", applyLocalInput), async (input) => {
770
+ try {
771
+ applyLocalInput.parse(input);
772
+ if (activeSetups.length > 0 || retiredSetups.length > 0) {
773
+ const applied = await applyLocalSetupBatch(root, activeSetups, retiredSetups);
774
+ const groups = applied.applied.map(({ setupId, result: appliedResult }) => ({
775
+ setupId: setupId,
776
+ status: "verified",
777
+ files: appliedResult.files,
778
+ }));
779
+ const retiredGroups = applied.retired;
780
+ localRemovalOnlyComplete =
781
+ groups.length === 0 && retiredGroups.length > 0;
782
+ retiredSetups = [];
783
+ return result("apply_local_setup", {
784
+ status: "verified",
785
+ summary: `Verified ${groups.length} current Integration binding${groups.length === 1 ? "" : "s"} and retired ${retiredGroups.length} removed local binding${retiredGroups.length === 1 ? "" : "s"}. Remote Integrations were left active.`,
786
+ files: [
787
+ ...new Set([
788
+ ...groups.flatMap(({ files }) => files),
789
+ ...retiredGroups.flatMap(({ files }) => files),
790
+ ]),
791
+ ],
792
+ ...(groups.length > 0 ? { groups } : {}),
793
+ ...(retiredGroups.length > 0 ? { retiredGroups } : {}),
794
+ localSetupComplete: true,
795
+ });
796
+ }
797
+ const plan = await currentPlan();
798
+ activePlan = plan;
799
+ const applied = await engine.applyLocal({
800
+ planFingerprint: plan.fingerprint,
801
+ });
802
+ if (applied.status === "source_changes_required") {
803
+ return result("apply_local_setup", {
804
+ status: "manual_required",
805
+ summary: "Automatic setup could not safely transform every reviewed operation. No local changes were kept. Use the manual integration guide or adjust the unsupported code shape before preparing setup again.",
806
+ reasons: applied.sourceChanges.map((change) => `${change.file}:${change.line} cannot be transformed automatically with ${change.adapter}.`),
807
+ localSetupComplete: false,
808
+ });
809
+ }
810
+ return result("apply_local_setup", {
811
+ status: "verified",
812
+ summary: "Local Seamward setup is source-verified and ready for remote connection review.",
813
+ files: applied.files,
814
+ localSetupComplete: true,
815
+ });
816
+ }
817
+ catch (error) {
818
+ return errorResult(error);
819
+ }
820
+ });
821
+ const reviewRemoteInput = setupMcpInputSchemas.review_remote_setup;
822
+ server.registerTool("review_remote_setup", config("review_remote_setup", reviewRemoteInput), async (input) => {
823
+ try {
824
+ const parsed = reviewRemoteInput.parse(input);
825
+ if (localRemovalOnlyComplete) {
826
+ return result("review_remote_setup", {
827
+ status: "not_applicable",
828
+ summary: "Removed local bindings are retired. Their remote Seamward Integrations remain active and no remote write is required.",
829
+ });
830
+ }
831
+ const provisioningCredential = credentialProvider
832
+ ? await credentialProvider()
833
+ : null;
834
+ if (provisioningCredential?.collectorSourceId) {
835
+ if (parsed.contractFile || parsed.declaredVersion) {
836
+ throw new SetupMcpOperationError("invalid_input");
837
+ }
838
+ const sourceSetups = activeSetups.length > 0
839
+ ? activeSetups.map((setup) => ({
840
+ ...setup,
841
+ setupId: setupIdForPlan(setup.plan),
842
+ }))
843
+ : [
844
+ await (async () => {
845
+ const plan = await currentPlan();
846
+ return {
847
+ engine,
848
+ name: plan.projectName,
849
+ plan,
850
+ setupId: setupIdForPlan(plan),
851
+ };
852
+ })(),
853
+ ];
854
+ const sourceSetupIds = new Set(sourceSetups.map(({ setupId }) => setupId));
855
+ if (parsed.integrationProposals.some(({ setupId }, index) => !sourceSetupIds.has(setupId) ||
856
+ parsed.integrationProposals.findIndex((candidate) => candidate.setupId === setupId) !== index)) {
857
+ throw new SetupMcpOperationError("invalid_input");
858
+ }
859
+ const proposalOverrides = new Map(parsed.integrationProposals.map((proposal) => [
860
+ proposal.setupId,
861
+ proposal,
862
+ ]));
863
+ const reviewedStateFingerprints = new Map();
864
+ for (const setup of sourceSetups) {
865
+ const verification = await setup.engine.verify({
866
+ planFingerprint: setup.plan.fingerprint,
867
+ });
868
+ if (!verification.sourceBackedComplete) {
869
+ return result("review_remote_setup", {
870
+ status: "blocked",
871
+ summary: "Finish and verify every local integration before connecting remotely.",
872
+ blockers: verification.blockers.map(({ code }) => code),
873
+ });
874
+ }
875
+ reviewedStateFingerprints.set(setup.setupId, verification.stateFingerprint);
876
+ }
877
+ const proposals = await Promise.all(sourceSetups.map(async (setup) => {
878
+ const override = proposalOverrides.get(setup.setupId);
879
+ const scope = setup.plan.integrationScope;
880
+ if (!scope)
881
+ throw new SetupMcpOperationError("integration_scope_mismatch");
882
+ const suggestion = await suggestedSetupIdentity(setup);
883
+ const proposedName = override?.name ?? suggestion.name;
884
+ const matchingExisting = (provisioningCredential.integrations ?? []).filter((integration) => integration.direction === scope.direction &&
885
+ integration.protocol === scope.protocol &&
886
+ integration.name.trim().toLocaleLowerCase("en-US") ===
887
+ proposedName.trim().toLocaleLowerCase("en-US"));
888
+ const preferredIntegrationId = override?.preferredIntegrationId ??
889
+ (matchingExisting.length === 1
890
+ ? matchingExisting[0].id
891
+ : undefined);
892
+ const matchedProvider = preferredIntegrationId
893
+ ? matchingExisting.find(({ id }) => id === preferredIntegrationId)?.provider
894
+ : undefined;
895
+ return {
896
+ direction: scope.direction,
897
+ name: proposedName,
898
+ ...(preferredIntegrationId ? { preferredIntegrationId } : {}),
899
+ protocol: scope.protocol,
900
+ provider: override?.provider ??
901
+ matchedProvider ??
902
+ suggestion.provider,
903
+ setupId: setup.setupId,
904
+ };
905
+ }));
906
+ const preview = await requestSetupApi({
907
+ apiKey: provisioningCredential.apiKey,
908
+ body: { proposals },
909
+ endpoint: provisioningCredential.endpoint,
910
+ pathname: "/v1/setup/integrations/preview",
911
+ schema: integrationPreviewResponseSchema,
912
+ });
913
+ pendingProvisioning = {
914
+ collectorSourceId: provisioningCredential.collectorSourceId,
915
+ effects: preview.effects,
916
+ fingerprint: preview.fingerprint,
917
+ operationId: preview.operationId,
918
+ setups: sourceSetups.map((setup) => ({
919
+ ...setup,
920
+ reviewedStateFingerprint: reviewedStateFingerprints.get(setup.setupId),
921
+ })),
922
+ };
923
+ pendingRemote = null;
924
+ pendingRemoteSet = [];
925
+ const effectsBySetupId = new Map(preview.effects.map((effect) => [effect.setupId, effect]));
926
+ return result("review_remote_setup", {
927
+ status: "ready",
928
+ summary: `Review the detected Seamward Integration effects for all ${sourceSetups.length} boundaries. You can edit the proposed name, provider, or preferred existing Integration before reviewing again.`,
929
+ effects: preview.effects.flatMap((effect) => [
930
+ `${effect.action === "create" ? "Create" : "Reuse"} ${effect.name} for ${effect.direction} ${effect.protocol}`,
931
+ ...(sourceSetups
932
+ .find(({ setupId }) => setupId === effect.setupId)
933
+ ?.plan.actions.some(({ type }) => type === "register_contract")
934
+ ? [
935
+ `${effect.name}: register and activate the reviewed contract after the Integration is available`,
936
+ ]
937
+ : []),
938
+ ]),
939
+ groups: sourceSetups.map((setup) => {
940
+ const effect = effectsBySetupId.get(setup.setupId);
941
+ if (!effect)
942
+ throw new SetupMcpOperationError("remote_response_invalid");
943
+ const contractFile = setup.plan.actions.find(({ type, file }) => type === "register_contract" && file)?.file;
944
+ return {
945
+ action: effect.action,
946
+ direction: effect.direction,
947
+ effects: [
948
+ effect.action === "create"
949
+ ? "Create this Integration in the selected application environment"
950
+ : "Reuse this existing Integration",
951
+ ...(contractFile
952
+ ? [
953
+ "Register and activate the reviewed contract after the Integration is available",
954
+ ]
955
+ : []),
956
+ ],
957
+ integrationId: effect.integrationId,
958
+ name: effect.name,
959
+ protocol: effect.protocol,
960
+ provider: effect.provider,
961
+ setupId: effect.setupId,
962
+ ...(contractFile ? { contractFile } : {}),
963
+ };
964
+ }),
965
+ });
966
+ }
967
+ if (activeSetups.length > 0) {
968
+ if (parsed.contractFile || parsed.declaredVersion) {
969
+ throw new SetupMcpOperationError("invalid_input");
970
+ }
971
+ const setupIds = new Set(activeSetups.map(({ plan }) => plan.setupId));
972
+ if (parsed.integrationMappings.some(({ setupId }, index) => !setupIds.has(setupId) ||
973
+ parsed.integrationMappings.findIndex((candidate) => candidate.setupId === setupId) !== index)) {
974
+ throw new SetupMcpOperationError("invalid_input");
975
+ }
976
+ reviewedIntegrationMappings = new Map(parsed.integrationMappings.map(({ setupId, integrationId }) => [
977
+ setupId,
978
+ integrationId,
979
+ ]));
980
+ const integrationChoices = [];
981
+ for (const setup of activeSetups) {
982
+ try {
983
+ await credentialsForPlan(setup.plan, setup.name);
984
+ }
985
+ catch (error) {
986
+ if (error instanceof SetupMcpOperationError &&
987
+ error.code === "integration_mapping_required") {
988
+ integrationChoices.push({
989
+ setupId: setup.plan.setupId,
990
+ name: setup.name,
991
+ candidates: await integrationCandidatesForPlan(setup.plan),
992
+ });
993
+ continue;
994
+ }
995
+ throw error;
996
+ }
997
+ }
998
+ if (integrationChoices.length > 0) {
999
+ return result("review_remote_setup", {
1000
+ status: "needs_input",
1001
+ summary: "Review which existing Seamward Integration belongs to each discovered boundary.",
1002
+ integrationChoices,
1003
+ });
1004
+ }
1005
+ const reviewed = [];
1006
+ const pending = [];
1007
+ for (const setup of activeSetups) {
1008
+ const verification = await setup.engine.verify({
1009
+ planFingerprint: setup.plan.fingerprint,
1010
+ });
1011
+ if (!verification.sourceBackedComplete) {
1012
+ return result("review_remote_setup", {
1013
+ status: "blocked",
1014
+ summary: "Finish and verify every local integration before connecting remotely.",
1015
+ blockers: verification.blockers.map(({ code }) => code),
1016
+ });
1017
+ }
1018
+ const contractFiles = setup.plan.actions.flatMap((action) => action.type === "register_contract" && action.file
1019
+ ? [action.file]
1020
+ : []);
1021
+ const credentials = await credentialsForPlan(setup.plan, setup.name);
1022
+ if (contractFiles.length === 0) {
1023
+ reviewed.push({
1024
+ setupId: setup.plan.setupId,
1025
+ integrationId: "integrationId" in credentials && credentials.integrationId
1026
+ ? credentials.integrationId
1027
+ : setup.plan.setupId,
1028
+ name: setup.name,
1029
+ effects: [
1030
+ "Use this verified collector binding without registering a contract",
1031
+ ],
1032
+ });
1033
+ continue;
1034
+ }
1035
+ if (contractFiles.length !== 1) {
1036
+ return result("review_remote_setup", {
1037
+ status: "needs_input",
1038
+ summary: `Choose one contract for ${setup.name}.`,
1039
+ contractChoices: contractFiles,
1040
+ });
1041
+ }
1042
+ const file = contractFiles[0];
1043
+ const declaredVersion = await deriveDeclaredVersion(file);
1044
+ const preview = await previewPlanContract(setup.plan, root, credentials.connectionKey, { file });
1045
+ const { match: activeMatch } = await readMatchingActivePlanContract(setup.plan, root, {
1046
+ apiKey: credentials.apiKey,
1047
+ connectionKey: credentials.connectionKey,
1048
+ declaredVersion,
1049
+ endpoint: credentials.endpoint,
1050
+ file,
1051
+ fetchFn,
1052
+ });
1053
+ const effects = activeMatch
1054
+ ? [
1055
+ "Link this exact already-active contract to the reviewed local binding",
1056
+ ]
1057
+ : [
1058
+ "Register one immutable contract draft",
1059
+ "Activate that reviewed contract for analysis",
1060
+ ];
1061
+ reviewed.push({
1062
+ setupId: setup.plan.setupId,
1063
+ integrationId: preview.integrationId,
1064
+ name: setup.name,
1065
+ contractFile: file,
1066
+ declaredVersion,
1067
+ effects,
1068
+ });
1069
+ pending.push({
1070
+ name: setup.name,
1071
+ integrationId: preview.integrationId,
1072
+ plan: setup.plan,
1073
+ file,
1074
+ declaredVersion,
1075
+ reviewedStateFingerprint: verification.stateFingerprint,
1076
+ ...(activeMatch
1077
+ ? { existingActiveContractVersionId: activeMatch.id }
1078
+ : {}),
1079
+ });
1080
+ }
1081
+ pendingRemoteSet = pending;
1082
+ pendingRemote = null;
1083
+ return result("review_remote_setup", {
1084
+ status: "ready",
1085
+ summary: `Review the remote effects for all ${reviewed.length} integrations.`,
1086
+ effects: reviewed.flatMap(({ name, effects }) => effects.map((effect) => `${name}: ${effect}`)),
1087
+ groups: reviewed,
1088
+ });
1089
+ }
1090
+ const plan = await currentPlan();
1091
+ const verification = await engine.verify({
1092
+ planFingerprint: plan.fingerprint,
1093
+ });
1094
+ if (!verification.sourceBackedComplete) {
1095
+ return result("review_remote_setup", {
1096
+ status: "blocked",
1097
+ summary: "Finish and verify the local source changes before connecting remotely.",
1098
+ blockers: verification.blockers.map(({ code }) => code),
1099
+ });
1100
+ }
1101
+ const contractFiles = plan.actions.flatMap((action) => action.type === "register_contract" && action.file
1102
+ ? [action.file]
1103
+ : []);
1104
+ if (contractFiles.length === 0) {
1105
+ return result("review_remote_setup", {
1106
+ status: "not_applicable",
1107
+ summary: "This setup has no contract to register. The collector can begin sending observations after its runtime credentials are configured.",
1108
+ });
1109
+ }
1110
+ if (!parsed.contractFile && contractFiles.length > 1) {
1111
+ return result("review_remote_setup", {
1112
+ status: "needs_input",
1113
+ summary: "Choose the reviewed contract to connect.",
1114
+ contractChoices: contractFiles,
1115
+ });
1116
+ }
1117
+ const file = parsed.contractFile ?? contractFiles[0];
1118
+ if (!contractFiles.includes(file)) {
1119
+ throw new SetupMcpOperationError("invalid_input");
1120
+ }
1121
+ const declaredVersion = parsed.declaredVersion ?? (await deriveDeclaredVersion(file));
1122
+ const { connectionKey, apiKey, endpoint } = await credentialsForPlan(plan);
1123
+ await previewPlanContract(plan, root, connectionKey, { file });
1124
+ const { match: activeMatch } = await readMatchingActivePlanContract(plan, root, {
1125
+ apiKey,
1126
+ connectionKey,
1127
+ declaredVersion,
1128
+ endpoint,
1129
+ file,
1130
+ fetchFn,
1131
+ });
1132
+ pendingRemote = {
1133
+ plan,
1134
+ file,
1135
+ declaredVersion,
1136
+ reviewedStateFingerprint: verification.stateFingerprint,
1137
+ ...(activeMatch
1138
+ ? { existingActiveContractVersionId: activeMatch.id }
1139
+ : {}),
1140
+ };
1141
+ return result("review_remote_setup", {
1142
+ status: "ready",
1143
+ summary: activeMatch
1144
+ ? `${file} version ${declaredVersion} is already active. Link it to this reviewed local setup without changing the remote contract.`
1145
+ : `Connect ${file} version ${declaredVersion} to this Seamward integration.`,
1146
+ contractFile: file,
1147
+ declaredVersion,
1148
+ effects: activeMatch
1149
+ ? [
1150
+ "Link this exact already-active contract to the reviewed local setup",
1151
+ "Make no remote contract or activation change",
1152
+ ]
1153
+ : [
1154
+ "Register one immutable contract draft",
1155
+ "Activate that reviewed contract for analysis",
1156
+ ],
1157
+ });
1158
+ }
1159
+ catch (error) {
1160
+ return errorResult(error);
1161
+ }
1162
+ });
1163
+ const connectRemoteInput = setupMcpInputSchemas.connect_remote_setup;
1164
+ server.registerTool("connect_remote_setup", config("connect_remote_setup", connectRemoteInput), async (input) => {
1165
+ try {
1166
+ connectRemoteInput.parse(input);
1167
+ if (pendingProvisioning) {
1168
+ if (!credentialProvider)
1169
+ throw new SetupMcpOperationError("configuration_required");
1170
+ const credential = await credentialProvider();
1171
+ if (credential.collectorSourceId !==
1172
+ pendingProvisioning.collectorSourceId) {
1173
+ throw new SetupMcpOperationError("setup_state_conflict");
1174
+ }
1175
+ for (const setup of pendingProvisioning.setups) {
1176
+ const verification = await setup.engine.verify({
1177
+ planFingerprint: setup.plan.fingerprint,
1178
+ });
1179
+ if (!verification.sourceBackedComplete ||
1180
+ verification.stateFingerprint !== setup.reviewedStateFingerprint) {
1181
+ throw new SetupMcpOperationError("setup_state_conflict");
1182
+ }
1183
+ }
1184
+ const applied = await requestSetupApi({
1185
+ apiKey: credential.apiKey,
1186
+ body: {
1187
+ fingerprint: pendingProvisioning.fingerprint,
1188
+ operationId: pendingProvisioning.operationId,
1189
+ },
1190
+ endpoint: credential.endpoint,
1191
+ idempotencyKey: `${pendingProvisioning.operationId}:${pendingProvisioning.fingerprint.slice(7, 23)}`,
1192
+ pathname: "/v1/setup/integrations/apply",
1193
+ schema: integrationApplyResponseSchema,
1194
+ });
1195
+ if (applied.operationId !== pendingProvisioning.operationId) {
1196
+ throw new SetupMcpOperationError("remote_response_invalid");
1197
+ }
1198
+ const integrationsBySetupId = new Map(applied.integrations.map((integration) => [
1199
+ integration.setupId,
1200
+ integration,
1201
+ ]));
1202
+ if (integrationsBySetupId.size !==
1203
+ pendingProvisioning.setups.length ||
1204
+ pendingProvisioning.setups.some(({ setupId }) => !integrationsBySetupId.has(setupId))) {
1205
+ throw new SetupMcpOperationError("remote_response_invalid");
1206
+ }
1207
+ reviewedIntegrationMappings = new Map(applied.integrations.map(({ id, setupId }) => [setupId, id]));
1208
+ const connected = [];
1209
+ for (const [index, setup] of pendingProvisioning.setups.entries()) {
1210
+ const integration = integrationsBySetupId.get(setup.setupId);
1211
+ try {
1212
+ const contractFiles = setup.plan.actions.flatMap((action) => action.type === "register_contract" && action.file
1213
+ ? [action.file]
1214
+ : []);
1215
+ if (contractFiles.length > 1) {
1216
+ throw new SetupMcpOperationError("invalid_input");
1217
+ }
1218
+ let outcome;
1219
+ if (contractFiles.length === 0) {
1220
+ outcome = {
1221
+ status: "connected",
1222
+ contractStatus: "not_applicable",
1223
+ };
1224
+ }
1225
+ else {
1226
+ const file = contractFiles[0];
1227
+ const declaredVersion = await deriveDeclaredVersion(file);
1228
+ await previewPlanContract(setup.plan, root, integration.connectionKey, { file });
1229
+ const { match: activeMatch } = await readMatchingActivePlanContract(setup.plan, root, {
1230
+ apiKey: credential.apiKey,
1231
+ connectionKey: integration.connectionKey,
1232
+ declaredVersion,
1233
+ endpoint: credential.endpoint,
1234
+ file,
1235
+ fetchFn,
1236
+ });
1237
+ outcome = await connectReviewedSetup({
1238
+ declaredVersion,
1239
+ ...(activeMatch
1240
+ ? { existingActiveContractVersionId: activeMatch.id }
1241
+ : {}),
1242
+ file,
1243
+ integrationId: integration.id,
1244
+ name: setup.name,
1245
+ plan: setup.plan,
1246
+ reviewedStateFingerprint: setup.reviewedStateFingerprint,
1247
+ });
1248
+ }
1249
+ await recordSetupIntegrationBinding(setup.plan, root, integration.id);
1250
+ connected.push({
1251
+ contractStatus: outcome.contractStatus,
1252
+ integrationId: integration.id,
1253
+ setupId: setup.setupId,
1254
+ status: outcome.status,
1255
+ });
1256
+ }
1257
+ catch (error) {
1258
+ const failed = {
1259
+ contractStatus: "unknown",
1260
+ errorCode: errorCode(error),
1261
+ integrationId: integration.id,
1262
+ setupId: setup.setupId,
1263
+ status: "failed",
1264
+ };
1265
+ const unattempted = pendingProvisioning.setups
1266
+ .slice(index + 1)
1267
+ .map((next) => ({
1268
+ contractStatus: "unknown",
1269
+ integrationId: integrationsBySetupId.get(next.setupId).id,
1270
+ setupId: next.setupId,
1271
+ status: "not_attempted",
1272
+ }));
1273
+ return result("connect_remote_setup", {
1274
+ status: "partially_connected",
1275
+ summary: "Integration provisioning completed, but one reviewed contract connection failed. Retry this approved operation to reconcile without creating duplicate Integrations.",
1276
+ contractStatus: "unknown",
1277
+ groups: [...connected, failed, ...unattempted],
1278
+ });
1279
+ }
1280
+ }
1281
+ pendingProvisioning = null;
1282
+ const partiallyConnected = connected.some(({ status }) => status === "partially_connected");
1283
+ return result("connect_remote_setup", {
1284
+ status: partiallyConnected ? "partially_connected" : "connected",
1285
+ summary: partiallyConnected
1286
+ ? "All Integrations are available, but at least one contract remains a draft."
1287
+ : `Connected all ${connected.length} detected Integrations.`,
1288
+ contractStatus: connected.every(({ contractStatus }) => contractStatus === "not_applicable")
1289
+ ? "not_applicable"
1290
+ : connected.every(({ contractStatus }) => contractStatus === "active")
1291
+ ? "active"
1292
+ : "draft",
1293
+ groups: connected,
1294
+ });
1295
+ }
1296
+ if (activeSetups.length > 0) {
1297
+ const noContract = activeSetups.filter(({ plan }) => !plan.actions.some(({ type }) => type === "register_contract"));
1298
+ if (pendingRemoteSet.length === 0 &&
1299
+ noContract.length !== activeSetups.length) {
1300
+ throw new SetupMcpOperationError("contract_preview_expired");
1301
+ }
1302
+ const pendingBySetupId = new Map(pendingRemoteSet.map((pending) => [
1303
+ pending.plan.setupId,
1304
+ pending,
1305
+ ]));
1306
+ const workItems = [];
1307
+ for (const setup of activeSetups) {
1308
+ const pending = pendingBySetupId.get(setup.plan.setupId);
1309
+ if (pending) {
1310
+ workItems.push({
1311
+ kind: "contract",
1312
+ setup,
1313
+ pending,
1314
+ integrationId: pending.integrationId,
1315
+ });
1316
+ continue;
1317
+ }
1318
+ const credentials = await credentialsForPlan(setup.plan, setup.name);
1319
+ workItems.push({
1320
+ kind: "collector",
1321
+ setup,
1322
+ integrationId: "integrationId" in credentials && credentials.integrationId
1323
+ ? credentials.integrationId
1324
+ : setup.plan.setupId,
1325
+ });
1326
+ }
1327
+ const connected = [];
1328
+ for (const [index, item] of workItems.entries()) {
1329
+ try {
1330
+ const outcome = item.kind === "contract"
1331
+ ? await connectReviewedSetup(item.pending)
1332
+ : {
1333
+ status: "already_connected",
1334
+ contractStatus: "not_applicable",
1335
+ };
1336
+ await recordSetupIntegrationBinding(item.setup.plan, root, item.integrationId);
1337
+ connected.push({
1338
+ setupId: item.setup.plan.setupId,
1339
+ integrationId: item.integrationId,
1340
+ ...outcome,
1341
+ });
1342
+ }
1343
+ catch (error) {
1344
+ const failed = {
1345
+ setupId: item.setup.plan.setupId,
1346
+ integrationId: item.integrationId,
1347
+ status: "failed",
1348
+ contractStatus: "unknown",
1349
+ errorCode: errorCode(error),
1350
+ };
1351
+ const unattempted = workItems.slice(index + 1).map((next) => ({
1352
+ setupId: next.setup.plan.setupId,
1353
+ integrationId: next.integrationId,
1354
+ status: "not_attempted",
1355
+ contractStatus: next.kind === "contract"
1356
+ ? "unknown"
1357
+ : "not_applicable",
1358
+ }));
1359
+ return result("connect_remote_setup", {
1360
+ status: "partially_connected",
1361
+ summary: "Remote connection stopped after one Integration failed. Completed Integrations remain recorded; retrying this reviewed batch reconciles them without creating duplicate contract versions.",
1362
+ contractStatus: "unknown",
1363
+ groups: [...connected, failed, ...unattempted],
1364
+ });
1365
+ }
1366
+ }
1367
+ pendingRemoteSet = [];
1368
+ const partial = connected.some(({ status }) => status === "partially_connected");
1369
+ return result("connect_remote_setup", {
1370
+ status: partial ? "partially_connected" : "connected",
1371
+ summary: partial
1372
+ ? "Some integrations connected, but at least one contract remains a draft. Review remote setup again before retrying."
1373
+ : `Connected all ${connected.length} integrations.`,
1374
+ contractStatus: connected.every(({ contractStatus }) => contractStatus === "not_applicable")
1375
+ ? "not_applicable"
1376
+ : connected.every(({ contractStatus }) => contractStatus === "active")
1377
+ ? "active"
1378
+ : "draft",
1379
+ groups: connected,
1380
+ });
1381
+ }
1382
+ if (!pendingRemote) {
1383
+ throw new SetupMcpOperationError("contract_preview_expired");
1384
+ }
1385
+ const { plan, file, declaredVersion, reviewedStateFingerprint, existingActiveContractVersionId, } = pendingRemote;
1386
+ const verification = await engine.verify({
1387
+ planFingerprint: plan.fingerprint,
1388
+ });
1389
+ if (!verification.sourceBackedComplete) {
1390
+ throw new SetupMcpOperationError("source_verification_required");
1391
+ }
1392
+ if (verification.stateFingerprint !== reviewedStateFingerprint) {
1393
+ const reconciledContractVersionId = existingActiveContractVersionId
1394
+ ? await registeredContractVersionIdForFile(plan, root, file)
1395
+ : null;
1396
+ const reconciledActivation = verification.remoteLifecycle.activation?.contractVersionId;
1397
+ const recoverableReconciliation = existingActiveContractVersionId !== undefined &&
1398
+ reconciledContractVersionId === existingActiveContractVersionId &&
1399
+ (reconciledActivation === undefined ||
1400
+ reconciledActivation === existingActiveContractVersionId);
1401
+ if (!recoverableReconciliation) {
1402
+ throw new SetupMcpOperationError("setup_state_conflict");
1403
+ }
1404
+ pendingRemote.reviewedStateFingerprint =
1405
+ verification.stateFingerprint;
1406
+ }
1407
+ const { connectionKey, apiKey, endpoint } = await credentialsForPlan(plan);
1408
+ if (existingActiveContractVersionId) {
1409
+ await reconcileActivePlanContract(plan, root, {
1410
+ apiKey,
1411
+ connectionKey,
1412
+ contractVersionId: existingActiveContractVersionId,
1413
+ declaredVersion,
1414
+ endpoint,
1415
+ file,
1416
+ fetchFn,
1417
+ });
1418
+ pendingRemote = null;
1419
+ return result("connect_remote_setup", {
1420
+ status: "already_connected",
1421
+ summary: "The exact reviewed contract was already active and is now linked to this local setup.",
1422
+ contractStatus: "active",
1423
+ });
1424
+ }
1425
+ let contractVersionId = await registeredContractVersionIdForFile(plan, root, file);
1426
+ if (!contractVersionId) {
1427
+ const registered = await registerPlanContracts(plan, root, {
1428
+ apiKey,
1429
+ connectionKey,
1430
+ declaredVersion,
1431
+ file,
1432
+ endpoint,
1433
+ fetchFn,
1434
+ writeCompletion: true,
1435
+ });
1436
+ contractVersionId =
1437
+ registered.registered[0]?.contractVersionId ?? null;
1438
+ if (contractVersionId) {
1439
+ pendingRemote.reviewedStateFingerprint = (await engine.verify({ planFingerprint: plan.fingerprint })).stateFingerprint;
1440
+ }
1441
+ }
1442
+ if (!contractVersionId) {
1443
+ return result("connect_remote_setup", {
1444
+ status: "partially_connected",
1445
+ summary: "The contract draft could not be confirmed. Review the remote setup again before retrying.",
1446
+ contractStatus: "draft",
1447
+ });
1448
+ }
1449
+ const status = await readContractStatus({
1450
+ apiKey,
1451
+ connectionKey,
1452
+ endpoint,
1453
+ fetchFn,
1454
+ });
1455
+ const activation = await previewPlanContractActivation(plan, root, status, { contractVersionId, reason: "promote" });
1456
+ const alreadyActive = activation.reconcile;
1457
+ await activatePlanContractVersion(plan, root, activation, {
1458
+ apiKey,
1459
+ connectionKey,
1460
+ endpoint,
1461
+ fetchFn,
1462
+ });
1463
+ pendingRemote = null;
1464
+ return result("connect_remote_setup", {
1465
+ status: alreadyActive ? "already_connected" : "connected",
1466
+ summary: alreadyActive
1467
+ ? "The reviewed contract was already active and is now reconciled with this local setup."
1468
+ : "The reviewed contract is registered and active.",
1469
+ contractStatus: "active",
1470
+ });
1471
+ }
1472
+ catch (error) {
1473
+ return errorResult(error);
1474
+ }
1475
+ });
1476
+ const statusInput = setupMcpInputSchemas.check_setup_status;
1477
+ server.registerTool("check_setup_status", config("check_setup_status", statusInput), async (input) => {
1478
+ try {
1479
+ statusInput.parse(input);
1480
+ if (activeSetups.length === 0 && activePlan === null) {
1481
+ const savedPlans = await readSetupPlans(root);
1482
+ if (savedPlans.length > 0) {
1483
+ activeSetups = await Promise.all(savedPlans.map(async (plan) => ({
1484
+ engine: await createLocalSetupEngine({
1485
+ root,
1486
+ setupId: plan.setupId,
1487
+ }),
1488
+ name: plan.setupId ?? "Integration",
1489
+ plan,
1490
+ })));
1491
+ }
1492
+ }
1493
+ if (activeSetups.length > 0) {
1494
+ const groups = [];
1495
+ for (const setup of activeSetups) {
1496
+ const verification = await setup.engine.verify({
1497
+ planFingerprint: setup.plan.fingerprint,
1498
+ });
1499
+ if (!verification.sourceBackedComplete) {
1500
+ groups.push({
1501
+ setupId: setup.plan.setupId,
1502
+ status: verification.instrumentationActions.some(({ status }) => status === "stale" || status === "missing")
1503
+ ? "stale"
1504
+ : "local_changes_required",
1505
+ contractStatus: "missing",
1506
+ firstObservation: "not_checked",
1507
+ });
1508
+ continue;
1509
+ }
1510
+ const credentials = await credentialsForPlan(setup.plan, setup.name);
1511
+ const hasContract = setup.plan.actions.some(({ type }) => type === "register_contract");
1512
+ let contractStatus = "not_applicable";
1513
+ if (hasContract) {
1514
+ const remote = await readContractStatus({
1515
+ apiKey: credentials.apiKey,
1516
+ connectionKey: credentials.connectionKey,
1517
+ endpoint: credentials.endpoint,
1518
+ fetchFn,
1519
+ });
1520
+ contractStatus = remote.activeContractVersionId
1521
+ ? "active"
1522
+ : remote.contracts.some(({ lifecycleStatus }) => lifecycleStatus === "draft")
1523
+ ? "draft"
1524
+ : "missing";
1525
+ if (contractStatus !== "active" ||
1526
+ !verification.remoteLifecycle.activation) {
1527
+ groups.push({
1528
+ setupId: setup.plan.setupId,
1529
+ status: "ready_to_connect",
1530
+ contractStatus,
1531
+ firstObservation: "not_checked",
1532
+ });
1533
+ continue;
1534
+ }
1535
+ }
1536
+ const observation = await checkFirstSetupObservation(setup.plan, root, {
1537
+ apiKey: credentials.apiKey,
1538
+ connectionKey: credentials.connectionKey,
1539
+ endpoint: credentials.endpoint,
1540
+ fetchFn,
1541
+ write: false,
1542
+ });
1543
+ const observed = observation.status === "observed";
1544
+ groups.push({
1545
+ setupId: setup.plan.setupId,
1546
+ status: observed
1547
+ ? "connected"
1548
+ : "waiting_for_traffic",
1549
+ contractStatus,
1550
+ firstObservation: observed
1551
+ ? "observed"
1552
+ : "waiting",
1553
+ });
1554
+ }
1555
+ const connected = groups.filter(({ status }) => status === "connected").length;
1556
+ const overallStatus = connected === groups.length
1557
+ ? "connected"
1558
+ : connected > 0
1559
+ ? "partially_connected"
1560
+ : groups.some(({ status }) => status === "local_changes_required")
1561
+ ? "local_changes_required"
1562
+ : groups.some(({ status }) => status === "stale")
1563
+ ? "stale"
1564
+ : groups.some(({ status }) => status === "ready_to_connect")
1565
+ ? "ready_to_connect"
1566
+ : "waiting_for_traffic";
1567
+ return result("check_setup_status", {
1568
+ status: overallStatus,
1569
+ summary: overallStatus === "connected"
1570
+ ? `All ${groups.length} integrations have accepted traffic.`
1571
+ : `${connected} of ${groups.length} integrations have accepted traffic.`,
1572
+ localSetupComplete: groups.every(({ status }) => status !== "local_changes_required" && status !== "stale"),
1573
+ contractStatus: groups.every(({ contractStatus }) => contractStatus === "not_applicable")
1574
+ ? "not_applicable"
1575
+ : groups.every(({ contractStatus }) => contractStatus === "active")
1576
+ ? "active"
1577
+ : groups.some(({ contractStatus }) => contractStatus === "draft")
1578
+ ? "draft"
1579
+ : "missing",
1580
+ firstObservation: connected === groups.length
1581
+ ? "observed"
1582
+ : connected > 0
1583
+ ? "waiting"
1584
+ : "not_checked",
1585
+ groups,
1586
+ });
1587
+ }
1588
+ let plan;
1589
+ try {
1590
+ plan = await currentPlan();
1591
+ }
1592
+ catch (error) {
1593
+ if (error instanceof LocalSetupEngineError &&
1594
+ error.code === "plan_not_found") {
1595
+ return result("check_setup_status", {
1596
+ status: "not_prepared",
1597
+ summary: "Run the Seamward setup preparation first.",
1598
+ localSetupComplete: false,
1599
+ contractStatus: "missing",
1600
+ firstObservation: "not_checked",
1601
+ });
1602
+ }
1603
+ throw error;
1604
+ }
1605
+ const verification = await engine.verify({
1606
+ planFingerprint: plan.fingerprint,
1607
+ });
1608
+ if (!verification.sourceBackedComplete) {
1609
+ const stale = verification.instrumentationActions.some(({ status }) => status === "stale" || status === "missing");
1610
+ return result("check_setup_status", {
1611
+ status: stale ? "stale" : "local_changes_required",
1612
+ summary: stale
1613
+ ? "Verified source changed after setup. Review and apply the local setup again."
1614
+ : "Local source instrumentation is not yet verified.",
1615
+ localSetupComplete: false,
1616
+ contractStatus: "missing",
1617
+ firstObservation: "not_checked",
1618
+ });
1619
+ }
1620
+ const { connectionKey, apiKey, endpoint } = await credentialsForPlan(plan);
1621
+ const contractActions = plan.actions.filter(({ type }) => type === "register_contract");
1622
+ let contractStatus = contractActions.length === 0 ? "not_applicable" : "missing";
1623
+ if (contractActions.length > 0) {
1624
+ const remote = await readContractStatus({
1625
+ apiKey,
1626
+ connectionKey,
1627
+ endpoint,
1628
+ fetchFn,
1629
+ });
1630
+ contractStatus = remote.activeContractVersionId
1631
+ ? "active"
1632
+ : remote.contracts.some(({ lifecycleStatus }) => lifecycleStatus === "draft")
1633
+ ? "draft"
1634
+ : "missing";
1635
+ const recordedActivation = verification.remoteLifecycle.activation?.contractVersionId;
1636
+ if (recordedActivation &&
1637
+ recordedActivation !== remote.activeContractVersionId) {
1638
+ return result("check_setup_status", {
1639
+ status: "conflict",
1640
+ summary: "The active remote contract changed after this local setup was connected. Review the remote connection again.",
1641
+ localSetupComplete: true,
1642
+ contractStatus,
1643
+ firstObservation: "not_checked",
1644
+ });
1645
+ }
1646
+ if (contractStatus !== "active") {
1647
+ return result("check_setup_status", {
1648
+ status: "ready_to_connect",
1649
+ summary: "Local setup is verified. Review the remote connection next.",
1650
+ localSetupComplete: true,
1651
+ contractStatus,
1652
+ firstObservation: "not_checked",
1653
+ });
1654
+ }
1655
+ if (!recordedActivation) {
1656
+ return result("check_setup_status", {
1657
+ status: "ready_to_connect",
1658
+ summary: "The active contract is not yet linked to this reviewed local setup. Review the remote connection next.",
1659
+ localSetupComplete: true,
1660
+ contractStatus,
1661
+ firstObservation: "not_checked",
1662
+ });
1663
+ }
1664
+ }
1665
+ const observation = await checkFirstSetupObservation(plan, root, {
1666
+ apiKey,
1667
+ connectionKey,
1668
+ endpoint,
1669
+ fetchFn,
1670
+ write: false,
1671
+ });
1672
+ if (observation.status === "contract_activation_required") {
1673
+ return result("check_setup_status", {
1674
+ status: "ready_to_connect",
1675
+ summary: "The active contract is not yet linked to this reviewed local setup. Review the remote connection next.",
1676
+ localSetupComplete: true,
1677
+ contractStatus,
1678
+ firstObservation: "not_checked",
1679
+ });
1680
+ }
1681
+ if (observation.status === "source_verification_required") {
1682
+ return result("check_setup_status", {
1683
+ status: "stale",
1684
+ summary: "Verified source changed after setup. Review and apply the local setup again.",
1685
+ localSetupComplete: false,
1686
+ contractStatus,
1687
+ firstObservation: "not_checked",
1688
+ });
1689
+ }
1690
+ const observed = observation.status === "observed";
1691
+ return result("check_setup_status", {
1692
+ status: observed ? "connected" : "waiting_for_traffic",
1693
+ summary: observed
1694
+ ? "Seamward has accepted traffic from this reviewed setup."
1695
+ : "Setup is connected and waiting for the first accepted observation.",
1696
+ localSetupComplete: true,
1697
+ contractStatus,
1698
+ firstObservation: observed ? "observed" : "waiting",
1699
+ });
1700
+ }
1701
+ catch (error) {
1702
+ return errorResult(error);
1703
+ }
1704
+ });
1705
+ server.registerPrompt("setup_seamward_integration", {
1706
+ title: "Set up Seamward in this project",
1707
+ description: "Prepare, apply, connect, and verify one service through two clear trust boundaries.",
1708
+ }, async () => ({
1709
+ messages: [
1710
+ {
1711
+ role: "user",
1712
+ content: {
1713
+ type: "text",
1714
+ text: "Set up Seamward in this project.",
1715
+ },
1716
+ },
1717
+ ],
1718
+ }));
1719
+ return server;
1720
+ }
1721
+ const analyzeInput = setupMcpInputSchemas.analyze_repository;
1722
+ server.registerTool("analyze_repository", config("analyze_repository", analyzeInput), async () => {
1723
+ try {
1724
+ return result("analyze_repository", await engine.analyze());
1725
+ }
1726
+ catch (error) {
1727
+ return errorResult(error);
1728
+ }
1729
+ });
1730
+ const previewContractInput = setupMcpInputSchemas.preview_contract_registration;
1731
+ server.registerTool("preview_contract_registration", config("preview_contract_registration", previewContractInput), async (input) => {
1732
+ try {
1733
+ const parsed = previewContractInput.parse(input);
1734
+ const plan = await readSetupPlan(root, parsed.planFingerprint);
1735
+ const connectionKey = env(plan.connectionKeyEnvironment);
1736
+ if (!connectionKey)
1737
+ throw new SetupMcpOperationError("configuration_required");
1738
+ const preview = await previewPlanContract(plan, root, connectionKey, {
1739
+ ...(parsed.file ? { file: parsed.file } : {}),
1740
+ includeExamples: parsed.includeExamples,
1741
+ ...(parsed.jsonSchemaBinding
1742
+ ? { jsonSchemaBinding: parsed.jsonSchemaBinding }
1743
+ : {}),
1744
+ });
1745
+ const request = {
1746
+ planFingerprint: parsed.planFingerprint,
1747
+ ...(parsed.file ? { file: parsed.file } : {}),
1748
+ declaredVersion: parsed.declaredVersion,
1749
+ includeExamples: parsed.includeExamples,
1750
+ ...(parsed.jsonSchemaBinding
1751
+ ? { jsonSchemaBinding: parsed.jsonSchemaBinding }
1752
+ : {}),
1753
+ };
1754
+ const fingerprint = approvalFingerprint({ request, preview });
1755
+ storeApprovalPreview(contractPreviews, fingerprint, request);
1756
+ return result("preview_contract_registration", {
1757
+ ...preview,
1758
+ approvalFingerprint: fingerprint,
1759
+ });
1760
+ }
1761
+ catch (error) {
1762
+ return errorResult(error);
1763
+ }
1764
+ });
1765
+ const registerContractInput = setupMcpInputSchemas.register_contract;
1766
+ server.registerTool("register_contract", config("register_contract", registerContractInput), async (input) => {
1767
+ try {
1768
+ const parsed = registerContractInput.parse(input);
1769
+ const approved = readApprovalPreview(contractPreviews, parsed.approvalFingerprint);
1770
+ if (!approved)
1771
+ throw new SetupMcpOperationError("contract_preview_expired");
1772
+ contractPreviews.delete(parsed.approvalFingerprint);
1773
+ const plan = await readSetupPlan(root, approved.planFingerprint);
1774
+ const connectionKey = env(plan.connectionKeyEnvironment);
1775
+ const apiKey = env("SEAMWARD_API_KEY");
1776
+ if (!connectionKey || !apiKey) {
1777
+ throw new SetupMcpOperationError("configuration_required");
1778
+ }
1779
+ const registered = await registerPlanContracts(plan, root, {
1780
+ apiKey,
1781
+ connectionKey,
1782
+ declaredVersion: approved.declaredVersion,
1783
+ endpoint: env("SEAMWARD_MANAGEMENT_API_URL"),
1784
+ fetchFn,
1785
+ ...(approved.file ? { file: approved.file } : {}),
1786
+ includeExamples: approved.includeExamples,
1787
+ writeCompletion: true,
1788
+ ...(approved.jsonSchemaBinding
1789
+ ? { jsonSchemaBinding: approved.jsonSchemaBinding }
1790
+ : {}),
1791
+ });
1792
+ return result("register_contract", registered);
1793
+ }
1794
+ catch (error) {
1795
+ return errorResult(error);
1796
+ }
1797
+ });
1798
+ const contractStatusForPlan = async (planFingerprint) => {
1799
+ const plan = await readSetupPlan(root, planFingerprint);
1800
+ const connectionKey = env(plan.connectionKeyEnvironment);
1801
+ const apiKey = env("SEAMWARD_API_KEY");
1802
+ if (!connectionKey || !apiKey) {
1803
+ throw new SetupMcpOperationError("configuration_required");
1804
+ }
1805
+ return {
1806
+ plan,
1807
+ connectionKey,
1808
+ status: await readContractStatus({
1809
+ apiKey,
1810
+ connectionKey,
1811
+ endpoint: env("SEAMWARD_MANAGEMENT_API_URL"),
1812
+ fetchFn,
1813
+ }),
1814
+ };
1815
+ };
1816
+ const contractStatusInput = setupMcpInputSchemas.read_contract_status;
1817
+ server.registerTool("read_contract_status", config("read_contract_status", contractStatusInput), async (input) => {
1818
+ try {
1819
+ const parsed = contractStatusInput.parse(input);
1820
+ return result("read_contract_status", (await contractStatusForPlan(parsed.planFingerprint)).status);
1821
+ }
1822
+ catch (error) {
1823
+ return errorResult(error);
1824
+ }
1825
+ });
1826
+ const previewActivationInput = setupMcpInputSchemas.preview_contract_activation;
1827
+ server.registerTool("preview_contract_activation", config("preview_contract_activation", previewActivationInput), async (input) => {
1828
+ try {
1829
+ const parsed = previewActivationInput.parse(input);
1830
+ const { plan, status } = await contractStatusForPlan(parsed.planFingerprint);
1831
+ const request = await previewPlanContractActivation(plan, root, status, {
1832
+ contractVersionId: parsed.contractVersionId,
1833
+ reason: parsed.reason,
1834
+ });
1835
+ const activationFingerprint = approvalFingerprint(request);
1836
+ storeApprovalPreview(activationPreviews, activationFingerprint, request);
1837
+ return result("preview_contract_activation", {
1838
+ ...request,
1839
+ approvalFingerprint: activationFingerprint,
1840
+ });
1841
+ }
1842
+ catch (error) {
1843
+ return errorResult(error);
1844
+ }
1845
+ });
1846
+ const activateInput = setupMcpInputSchemas.activate_contract;
1847
+ server.registerTool("activate_contract", config("activate_contract", activateInput), async (input) => {
1848
+ try {
1849
+ const parsed = activateInput.parse(input);
1850
+ const approved = readApprovalPreview(activationPreviews, parsed.approvalFingerprint);
1851
+ if (!approved)
1852
+ throw new SetupMcpOperationError("contract_preview_expired");
1853
+ activationPreviews.delete(parsed.approvalFingerprint);
1854
+ const plan = await readSetupPlan(root, approved.planFingerprint);
1855
+ const connectionKey = env(plan.connectionKeyEnvironment);
1856
+ const apiKey = env("SEAMWARD_API_KEY");
1857
+ if (!connectionKey || !apiKey) {
1858
+ throw new SetupMcpOperationError("configuration_required");
1859
+ }
1860
+ const activated = await activatePlanContractVersion(plan, root, approved, {
1861
+ apiKey,
1862
+ connectionKey,
1863
+ endpoint: env("SEAMWARD_MANAGEMENT_API_URL"),
1864
+ fetchFn,
1865
+ });
1866
+ return result("activate_contract", activated);
1867
+ }
1868
+ catch (error) {
1869
+ return errorResult(error);
1870
+ }
1871
+ });
1872
+ const observationInput = setupMcpInputSchemas.check_first_observation;
1873
+ server.registerTool("check_first_observation", config("check_first_observation", observationInput), async (input) => {
1874
+ try {
1875
+ const parsed = observationInput.parse(input);
1876
+ const plan = await readSetupPlan(root, parsed.planFingerprint);
1877
+ const connectionKey = env(plan.connectionKeyEnvironment);
1878
+ const apiKey = env("SEAMWARD_API_KEY");
1879
+ if (!connectionKey || !apiKey) {
1880
+ throw new SetupMcpOperationError("configuration_required");
1881
+ }
1882
+ const checked = await checkFirstSetupObservation(plan, root, {
1883
+ apiKey,
1884
+ connectionKey,
1885
+ endpoint: env("SEAMWARD_MANAGEMENT_API_URL"),
1886
+ fetchFn,
1887
+ write: true,
1888
+ });
1889
+ if (checked.status !== "observed")
1890
+ return result("check_first_observation", checked);
1891
+ const { recorded: _recorded, ...publicResult } = checked;
1892
+ return result("check_first_observation", publicResult);
1893
+ }
1894
+ catch (error) {
1895
+ return errorResult(error);
1896
+ }
1897
+ });
1898
+ const planInput = setupMcpInputSchemas.plan_setup;
1899
+ server.registerTool("plan_setup", config("plan_setup", planInput), async (input) => {
1900
+ try {
1901
+ return result("plan_setup", await engine.plan(planInput.parse(input)));
1902
+ }
1903
+ catch (error) {
1904
+ return errorResult(error);
1905
+ }
1906
+ });
1907
+ const applyInput = setupMcpInputSchemas.apply_generated_setup;
1908
+ server.registerTool("apply_generated_setup", config("apply_generated_setup", applyInput), async (input) => {
1909
+ try {
1910
+ return result("apply_generated_setup", await engine.applyGenerated(applyInput.parse(input)));
1911
+ }
1912
+ catch (error) {
1913
+ return errorResult(error);
1914
+ }
1915
+ });
1916
+ const replaceInput = setupMcpInputSchemas.replace_generated_setup;
1917
+ server.registerTool("replace_generated_setup", config("replace_generated_setup", replaceInput), async (input) => {
1918
+ try {
1919
+ return result("replace_generated_setup", await engine.replaceGenerated(replaceInput.parse(input)));
1920
+ }
1921
+ catch (error) {
1922
+ return errorResult(error);
1923
+ }
1924
+ });
1925
+ const evidenceInput = setupMcpInputSchemas.record_setup_evidence;
1926
+ server.registerTool("record_setup_evidence", config("record_setup_evidence", evidenceInput), async (input) => {
1927
+ try {
1928
+ return result("record_setup_evidence", await engine.recordEvidence(evidenceInput.parse(input)));
1929
+ }
1930
+ catch (error) {
1931
+ return errorResult(error);
1932
+ }
1933
+ });
1934
+ const verifyInput = setupMcpInputSchemas.verify_setup;
1935
+ server.registerTool("verify_setup", config("verify_setup", verifyInput), async (input) => {
1936
+ try {
1937
+ return result("verify_setup", await engine.verify(verifyInput.parse(input)));
1938
+ }
1939
+ catch (error) {
1940
+ return errorResult(error);
1941
+ }
1942
+ });
1943
+ server.registerPrompt("setup_seamward_integration", {
1944
+ title: "Set up Seamward in this service",
1945
+ description: "Safe agent workflow for analyzing, editing, reviewing, and verifying one service.",
1946
+ }, async () => ({
1947
+ messages: [
1948
+ {
1949
+ role: "user",
1950
+ content: {
1951
+ type: "text",
1952
+ text: "Analyze this configured service root. Present the proposed integration scope and source edits before changing application files. Use your native workspace editing tools only after approval. Run the project tests with your normal tools. Then record source-backed evidence and verify setup. Do not read or edit environment files. Preview and register contracts only after separate approval, activate a contract only after explicit approval, then check for the first accepted observation.",
1953
+ },
1954
+ },
1955
+ ],
1956
+ }));
1957
+ return server;
1958
+ }
1959
+ //# sourceMappingURL=server.js.map