specshield-mcp-server 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +114 -0
  3. package/dist/backendTypes.d.ts +53 -0
  4. package/dist/backendTypes.js +6 -0
  5. package/dist/backendTypes.js.map +1 -0
  6. package/dist/client.d.ts +35 -0
  7. package/dist/client.js +93 -0
  8. package/dist/client.js.map +1 -0
  9. package/dist/config.d.ts +13 -0
  10. package/dist/config.js +30 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/errors.d.ts +25 -0
  13. package/dist/errors.js +24 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +23 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/logger.d.ts +14 -0
  19. package/dist/logger.js +25 -0
  20. package/dist/logger.js.map +1 -0
  21. package/dist/resources/help.d.ts +1 -0
  22. package/dist/resources/help.js +33 -0
  23. package/dist/resources/help.js.map +1 -0
  24. package/dist/server.d.ts +15 -0
  25. package/dist/server.js +31 -0
  26. package/dist/server.js.map +1 -0
  27. package/dist/specResolver.d.ts +11 -0
  28. package/dist/specResolver.js +23 -0
  29. package/dist/specResolver.js.map +1 -0
  30. package/dist/tools/compareSpecs.d.ts +6 -0
  31. package/dist/tools/compareSpecs.js +31 -0
  32. package/dist/tools/compareSpecs.js.map +1 -0
  33. package/dist/tools/explainBreakingChanges.d.ts +8 -0
  34. package/dist/tools/explainBreakingChanges.js +37 -0
  35. package/dist/tools/explainBreakingChanges.js.map +1 -0
  36. package/dist/tools/generateMigrationGuide.d.ts +6 -0
  37. package/dist/tools/generateMigrationGuide.js +36 -0
  38. package/dist/tools/generateMigrationGuide.js.map +1 -0
  39. package/dist/tools/generateReleaseNotes.d.ts +5 -0
  40. package/dist/tools/generateReleaseNotes.js +22 -0
  41. package/dist/tools/generateReleaseNotes.js.map +1 -0
  42. package/dist/tools/index.d.ts +10 -0
  43. package/dist/tools/index.js +21 -0
  44. package/dist/tools/index.js.map +1 -0
  45. package/dist/tools/isChangeSafe.d.ts +5 -0
  46. package/dist/tools/isChangeSafe.js +31 -0
  47. package/dist/tools/isChangeSafe.js.map +1 -0
  48. package/dist/tools/shared.d.ts +33 -0
  49. package/dist/tools/shared.js +57 -0
  50. package/dist/tools/shared.js.map +1 -0
  51. package/package.json +67 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SpecShield Software Private Limited
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # SpecShield MCP Server
2
+
3
+ **The API-change deploy gate for AI coding agents.** Ask *"is it safe to ship this
4
+ API change to my consumers?"* right inside Claude, Cursor, and other MCP clients —
5
+ and catch breaking changes before they reach your consumers.
6
+
7
+ It's a thin adapter over the [SpecShield](https://specshield.io) backend. Every tool
8
+ is **read-only / analyze-only — it never modifies your code.**
9
+
10
+ > Why not just diff specs? Plenty of tools (including free ones) list breaking
11
+ > changes. SpecShield's job is the *decision*: **can I deploy this?** — the deploy
12
+ > gate is the hero tool here.
13
+
14
+ > ⚙️ **In CI/CD instead of an agent?** The [`specshield` CLI](https://www.npmjs.com/package/specshield) runs the same breaking-change and `can-i-deploy` checks in your pipeline (GitHub Action, exit codes). Same job, two entry points: this server for AI agents, the CLI for CI/CD.
15
+
16
+ ## Tools
17
+
18
+ | # | Tool | What it answers |
19
+ |---|------|-----------------|
20
+ | 1 | **`is_change_safe`** ⭐ | Is this change safe to merge/deploy? Will it break consumers? (`safeToMerge` + risk + blocking reasons) |
21
+ | 2 | `explain_breaking_changes` | What breaks, developer & consumer impact, suggested migration |
22
+ | 3 | `generate_migration_guide` | Migration guide (markdown) + safe rollout steps |
23
+ | 4 | `generate_release_notes` | Release notes for developer / customer / internal |
24
+ | 5 | `compare_specs` | The raw diff (breaking / additions / modifications / warnings) + risk score |
25
+
26
+ Each tool accepts specs inline (`baseSpecContent` / `targetSpecContent`) or by path
27
+ (`baseSpecPath` / `targetSpecPath`).
28
+
29
+ _`run_governance_review` is planned — it ships once the backend governance endpoint
30
+ lands._
31
+
32
+ > Full setup, verification & troubleshooting: **[docs/mcp-server-setup.md](docs/mcp-server-setup.md)**.
33
+
34
+ ## Install
35
+
36
+ Requires **Node.js ≥ 20** and a SpecShield API key (from
37
+ [specshield.io/account](https://specshield.io/account)).
38
+
39
+ ```bash
40
+ npx -y specshield-mcp-server
41
+ ```
42
+
43
+ ### Claude Desktop
44
+ `claude_desktop_config.json`:
45
+ ```json
46
+ {
47
+ "mcpServers": {
48
+ "specshield": {
49
+ "command": "npx",
50
+ "args": ["-y", "specshield-mcp-server"],
51
+ "env": { "SPECSHIELD_API_KEY": "ss_your_key_here" }
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ### Claude Code
58
+ ```bash
59
+ claude mcp add specshield --env SPECSHIELD_API_KEY=ss_your_key_here -- npx -y specshield-mcp-server
60
+ ```
61
+
62
+ ### Cursor
63
+ `~/.cursor/mcp.json` (or the project `.cursor/mcp.json`):
64
+ ```json
65
+ {
66
+ "mcpServers": {
67
+ "specshield": {
68
+ "command": "npx",
69
+ "args": ["-y", "specshield-mcp-server"],
70
+ "env": { "SPECSHIELD_API_KEY": "ss_your_key_here" }
71
+ }
72
+ }
73
+ }
74
+ ```
75
+
76
+ ## Configuration
77
+
78
+ | Env var | Required | Default | Purpose |
79
+ |---------|----------|---------|---------|
80
+ | `SPECSHIELD_API_KEY` | **yes** | — | Your SpecShield API key. Store it as a secret; never commit it. |
81
+ | `SPECSHIELD_API_URL` | no | `https://api.specshield.io` | Backend base URL (override for self-hosted/staging). |
82
+ | `SPECSHIELD_TIMEOUT_MS` | no | `30000` | Per-request timeout. |
83
+ | `SPECSHIELD_LOG_LEVEL` | no | `info` | `debug` \| `info` \| `warn` \| `error` (logs go to stderr). |
84
+
85
+ ## Example prompts (lead with the deploy gate)
86
+
87
+ - *"Here are my old and new `openapi.yaml` — **is it safe to ship this API change to my consumers?**"*
88
+ - *"Compare `v1.yaml` and `v2.yaml` and tell me if I can deploy, and why not."*
89
+ - *"Explain the breaking changes between these two specs and how consumers should migrate."*
90
+ - *"Generate customer-facing release notes for this API change."*
91
+
92
+ ## Security & privacy
93
+
94
+ - **API key required.** Sent only as the `X-Api-Key` header to your configured backend.
95
+ - **Read-only / analyze-only.** No mutation tools, no shell execution, no arbitrary
96
+ file access (a spec file is read only when you explicitly pass a path).
97
+ - **No secret or spec logging.** The server never logs spec content, API keys, or
98
+ request bodies; error messages are redacted and machine-readable.
99
+ - Specs are sent to your configured SpecShield backend for analysis.
100
+
101
+ ## Local development
102
+
103
+ ```bash
104
+ npm install
105
+ npm run build # tsc → dist/
106
+ npm test # vitest (no network)
107
+ npm run lint
108
+ npm run smoke # boots the server against a stubbed backend and lists tools
109
+ npm start # run the built server over stdio
110
+ ```
111
+
112
+ ## License
113
+
114
+ MIT © SpecShield Software Private Limited
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Shapes returned by the SpecShield backend /api/intelligence/** endpoints
3
+ * (Phase 1 facade). Kept in one place so the tools stay thin mappers.
4
+ */
5
+ export type RiskLevel = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
6
+ export interface ChangeItem {
7
+ type: string;
8
+ changeId: string;
9
+ path: string | null;
10
+ method: string | null;
11
+ field: string | null;
12
+ oldValue: string | null;
13
+ newValue: string | null;
14
+ description: string | null;
15
+ severity: string;
16
+ }
17
+ export interface RiskScore {
18
+ level: RiskLevel;
19
+ score: number;
20
+ breakingCount: number;
21
+ totalChanges: number;
22
+ summary: string;
23
+ }
24
+ /** POST /api/intelligence/change-safety */
25
+ export interface ChangeSafetyResponse {
26
+ safeToMerge: boolean;
27
+ riskLevel: RiskLevel;
28
+ blockingReasons: string[];
29
+ recommendedAction: string;
30
+ reportUrl?: string;
31
+ environment?: string;
32
+ }
33
+ /** POST /api/intelligence/compare */
34
+ export interface IntelligenceCompareResponse {
35
+ breakingChanges: ChangeItem[];
36
+ additions: ChangeItem[];
37
+ modifications: ChangeItem[];
38
+ warnings: ChangeItem[];
39
+ risk: RiskScore;
40
+ safeToMerge: boolean;
41
+ compatibilitySummary: string;
42
+ }
43
+ /** POST /api/intelligence/release-notes-preview */
44
+ export interface ReleaseNotesPreview {
45
+ audience: string;
46
+ markdown: string;
47
+ }
48
+ /** POST /api/intelligence/migration-guide-preview */
49
+ export interface MigrationAdvice {
50
+ summary: string;
51
+ steps: string[];
52
+ language: string;
53
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Shapes returned by the SpecShield backend /api/intelligence/** endpoints
3
+ * (Phase 1 facade). Kept in one place so the tools stay thin mappers.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=backendTypes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backendTypes.js","sourceRoot":"","sources":["../src/backendTypes.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,35 @@
1
+ import type { SpecShieldConfig } from "./config.js";
2
+ import type { Logger } from "./logger.js";
3
+ export interface ApiClientOptions {
4
+ /** Total attempts including the first (retries = maxAttempts - 1). */
5
+ maxAttempts?: number;
6
+ /** Base backoff in ms; delay grows linearly per attempt. 0 disables sleeping (tests). */
7
+ baseDelayMs?: number;
8
+ /** Injectable fetch + sleep for tests. */
9
+ fetchImpl?: typeof fetch;
10
+ sleepImpl?: (ms: number) => Promise<void>;
11
+ }
12
+ /**
13
+ * Thin HTTP adapter over the SpecShield backend `/api/intelligence/**` endpoints.
14
+ * It holds NO diff/risk logic — every result comes from the backend.
15
+ *
16
+ * Contract:
17
+ * - sends `X-Api-Key` on every request;
18
+ * - retries only transient 429 / 5xx / network / timeout, up to `maxAttempts`;
19
+ * - never retries 4xx validation/auth errors;
20
+ * - enforces `SPECSHIELD_TIMEOUT_MS`;
21
+ * - throws {@link SpecShieldError} whose message never contains the spec, the
22
+ * API key, or the request body.
23
+ */
24
+ export declare class SpecShieldApiClient {
25
+ private readonly config;
26
+ private readonly logger;
27
+ private readonly maxAttempts;
28
+ private readonly baseDelayMs;
29
+ private readonly fetchImpl;
30
+ private readonly sleep;
31
+ constructor(config: SpecShieldConfig, logger: Logger, options?: ApiClientOptions);
32
+ /** POST a JSON body and return the parsed JSON response. */
33
+ post<T>(path: string, body: unknown): Promise<T>;
34
+ private backoff;
35
+ }
package/dist/client.js ADDED
@@ -0,0 +1,93 @@
1
+ import { SpecShieldError } from "./errors.js";
2
+ /**
3
+ * Thin HTTP adapter over the SpecShield backend `/api/intelligence/**` endpoints.
4
+ * It holds NO diff/risk logic — every result comes from the backend.
5
+ *
6
+ * Contract:
7
+ * - sends `X-Api-Key` on every request;
8
+ * - retries only transient 429 / 5xx / network / timeout, up to `maxAttempts`;
9
+ * - never retries 4xx validation/auth errors;
10
+ * - enforces `SPECSHIELD_TIMEOUT_MS`;
11
+ * - throws {@link SpecShieldError} whose message never contains the spec, the
12
+ * API key, or the request body.
13
+ */
14
+ export class SpecShieldApiClient {
15
+ config;
16
+ logger;
17
+ maxAttempts;
18
+ baseDelayMs;
19
+ fetchImpl;
20
+ sleep;
21
+ constructor(config, logger, options = {}) {
22
+ this.config = config;
23
+ this.logger = logger;
24
+ this.maxAttempts = options.maxAttempts ?? 3;
25
+ this.baseDelayMs = options.baseDelayMs ?? 300;
26
+ this.fetchImpl = options.fetchImpl ?? fetch;
27
+ this.sleep = options.sleepImpl ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
28
+ }
29
+ /** POST a JSON body and return the parsed JSON response. */
30
+ async post(path, body) {
31
+ const url = `${this.config.apiUrl}${path}`;
32
+ for (let attempt = 1;; attempt++) {
33
+ const controller = new AbortController();
34
+ const timer = setTimeout(() => controller.abort(), this.config.timeoutMs);
35
+ try {
36
+ const res = await this.fetchImpl(url, {
37
+ method: "POST",
38
+ headers: {
39
+ "Content-Type": "application/json",
40
+ "X-Api-Key": this.config.apiKey,
41
+ "X-SpecShield-Client": "mcp",
42
+ },
43
+ body: JSON.stringify(body),
44
+ signal: controller.signal,
45
+ });
46
+ if (res.ok) {
47
+ return (await res.json());
48
+ }
49
+ const status = res.status;
50
+ // 4xx auth/validation: never retry.
51
+ if (status === 401 || status === 403) {
52
+ throw new SpecShieldError("auth_error", "SpecShield rejected the API key. Check SPECSHIELD_API_KEY.", status);
53
+ }
54
+ if (status >= 400 && status < 500 && status !== 429) {
55
+ throw new SpecShieldError("validation_error", `SpecShield rejected the request as invalid (HTTP ${status}). ` +
56
+ "Check that both specs are valid OpenAPI.", status);
57
+ }
58
+ // 429 / 5xx: retry if attempts remain.
59
+ if (attempt < this.maxAttempts) {
60
+ this.logger.debug("retrying after transient status", { status, attempt });
61
+ await this.backoff(attempt);
62
+ continue;
63
+ }
64
+ throw new SpecShieldError(status === 429 ? "rate_limited" : "backend_error", `SpecShield request failed after ${this.maxAttempts} attempts (HTTP ${status}).`, status);
65
+ }
66
+ catch (err) {
67
+ if (err instanceof SpecShieldError)
68
+ throw err;
69
+ const aborted = err instanceof Error && err.name === "AbortError";
70
+ if (attempt < this.maxAttempts) {
71
+ this.logger.debug("retrying after transient error", {
72
+ attempt,
73
+ kind: aborted ? "timeout" : "network",
74
+ });
75
+ await this.backoff(attempt);
76
+ continue;
77
+ }
78
+ if (aborted) {
79
+ throw new SpecShieldError("timeout", `SpecShield request timed out after ${this.config.timeoutMs}ms.`);
80
+ }
81
+ throw new SpecShieldError("network_error", "Could not reach SpecShield.");
82
+ }
83
+ finally {
84
+ clearTimeout(timer);
85
+ }
86
+ }
87
+ }
88
+ async backoff(attempt) {
89
+ if (this.baseDelayMs > 0)
90
+ await this.sleep(this.baseDelayMs * attempt);
91
+ }
92
+ }
93
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export interface SpecShieldConfig {
3
+ apiUrl: string;
4
+ apiKey: string;
5
+ timeoutMs: number;
6
+ logLevel: LogLevel;
7
+ }
8
+ /**
9
+ * Loads configuration from environment variables. SPECSHIELD_API_KEY is required;
10
+ * everything else has a sensible default. Throws {@link ConfigError} (never
11
+ * echoing the key) when the key is missing.
12
+ */
13
+ export declare function loadConfig(env?: NodeJS.ProcessEnv): SpecShieldConfig;
package/dist/config.js ADDED
@@ -0,0 +1,30 @@
1
+ import { ConfigError } from "./errors.js";
2
+ const DEFAULT_API_URL = "https://api.specshield.io";
3
+ const DEFAULT_TIMEOUT_MS = 30_000;
4
+ const LOG_LEVELS = ["debug", "info", "warn", "error"];
5
+ /**
6
+ * Loads configuration from environment variables. SPECSHIELD_API_KEY is required;
7
+ * everything else has a sensible default. Throws {@link ConfigError} (never
8
+ * echoing the key) when the key is missing.
9
+ */
10
+ export function loadConfig(env = process.env) {
11
+ const apiKey = env.SPECSHIELD_API_KEY?.trim();
12
+ if (!apiKey) {
13
+ throw new ConfigError("SPECSHIELD_API_KEY is required. Set it to a SpecShield API key from " +
14
+ "https://specshield.io/account (store it as a secret; never commit it).");
15
+ }
16
+ const apiUrl = (env.SPECSHIELD_API_URL?.trim() || DEFAULT_API_URL).replace(/\/+$/, "");
17
+ const timeoutMs = parsePositiveInt(env.SPECSHIELD_TIMEOUT_MS, DEFAULT_TIMEOUT_MS);
18
+ const rawLevel = env.SPECSHIELD_LOG_LEVEL?.trim().toLowerCase();
19
+ const logLevel = LOG_LEVELS.includes(rawLevel ?? "")
20
+ ? rawLevel
21
+ : "info";
22
+ return { apiUrl, apiKey, timeoutMs, logLevel };
23
+ }
24
+ function parsePositiveInt(raw, fallback) {
25
+ if (!raw)
26
+ return fallback;
27
+ const n = Number.parseInt(raw, 10);
28
+ return Number.isFinite(n) && n > 0 ? n : fallback;
29
+ }
30
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAW1C,MAAM,eAAe,GAAG,2BAA2B,CAAC;AACpD,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAClC,MAAM,UAAU,GAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAE3E;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,MAAyB,OAAO,CAAC,GAAG;IAC7D,MAAM,MAAM,GAAG,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,CAAC;IAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,WAAW,CACnB,sEAAsE;YACpE,wEAAwE,CAC3E,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,IAAI,EAAE,IAAI,eAAe,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAEvF,MAAM,SAAS,GAAG,gBAAgB,CAAC,GAAG,CAAC,qBAAqB,EAAE,kBAAkB,CAAC,CAAC;IAElF,MAAM,QAAQ,GAAG,GAAG,CAAC,oBAAoB,EAAE,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IAChE,MAAM,QAAQ,GAAc,UAAgC,CAAC,QAAQ,CAAC,QAAQ,IAAI,EAAE,CAAC;QACnF,CAAC,CAAE,QAAqB;QACxB,CAAC,CAAC,MAAM,CAAC;IAEX,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;AACjD,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAuB,EAAE,QAAgB;IACjE,IAAI,CAAC,GAAG;QAAE,OAAO,QAAQ,CAAC;IAC1B,MAAM,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACnC,OAAO,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;AACpD,CAAC"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Machine-readable error codes surfaced to the MCP client. Deliberately coarse
3
+ * and free of any spec content or credentials.
4
+ */
5
+ export type ErrorCode = "config_error" | "validation_error" | "auth_error" | "rate_limited" | "backend_error" | "timeout" | "network_error" | "not_available";
6
+ /**
7
+ * The only error type this server throws outward. Its message is safe to show
8
+ * an end user: it never contains spec content, API keys, JWTs, or request bodies.
9
+ */
10
+ export declare class SpecShieldError extends Error {
11
+ readonly code: ErrorCode;
12
+ readonly status?: number | undefined;
13
+ constructor(code: ErrorCode, message: string, status?: number | undefined);
14
+ /** Structured, safe payload for a tool's `isError` result. */
15
+ toPayload(): {
16
+ error: {
17
+ code: ErrorCode;
18
+ message: string;
19
+ status?: number;
20
+ };
21
+ };
22
+ }
23
+ export declare class ConfigError extends SpecShieldError {
24
+ constructor(message: string);
25
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The only error type this server throws outward. Its message is safe to show
3
+ * an end user: it never contains spec content, API keys, JWTs, or request bodies.
4
+ */
5
+ export class SpecShieldError extends Error {
6
+ code;
7
+ status;
8
+ constructor(code, message, status) {
9
+ super(message);
10
+ this.code = code;
11
+ this.status = status;
12
+ this.name = "SpecShieldError";
13
+ }
14
+ /** Structured, safe payload for a tool's `isError` result. */
15
+ toPayload() {
16
+ return { error: { code: this.code, message: this.message, status: this.status } };
17
+ }
18
+ }
19
+ export class ConfigError extends SpecShieldError {
20
+ constructor(message) {
21
+ super("config_error", message);
22
+ }
23
+ }
24
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAcA;;;GAGG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAEtB;IAEA;IAHlB,YACkB,IAAe,EAC/B,OAAe,EACC,MAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAJC,SAAI,GAAJ,IAAI,CAAW;QAEf,WAAM,GAAN,MAAM,CAAS;QAG/B,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;IAED,8DAA8D;IAC9D,SAAS;QACP,OAAO,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;IACpF,CAAC;CACF;AAED,MAAM,OAAO,WAAY,SAAQ,eAAe;IAC9C,YAAY,OAAe;QACzB,KAAK,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IACjC,CAAC;CACF"}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { loadConfig } from "./config.js";
4
+ import { createLogger } from "./logger.js";
5
+ import { SpecShieldApiClient } from "./client.js";
6
+ import { createServer } from "./server.js";
7
+ import { SpecShieldError } from "./errors.js";
8
+ async function main() {
9
+ const config = loadConfig();
10
+ const logger = createLogger(config.logLevel);
11
+ const client = new SpecShieldApiClient(config, logger);
12
+ const server = createServer({ client, logger });
13
+ const transport = new StdioServerTransport();
14
+ await server.connect(transport);
15
+ // apiUrl is safe to log; the API key is never logged.
16
+ logger.info("specshield-mcp-server started (stdio)", { apiUrl: config.apiUrl });
17
+ }
18
+ main().catch((err) => {
19
+ const message = err instanceof SpecShieldError ? err.message : "Failed to start specshield-mcp-server.";
20
+ process.stderr.write(`[specshield-mcp] fatal: ${message}\n`);
21
+ process.exit(1);
22
+ });
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,KAAK,UAAU,IAAI;IACjB,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,IAAI,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACvD,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAEhD,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;IAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;IAChC,sDAAsD;IACtD,MAAM,CAAC,IAAI,CAAC,uCAAuC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;AAClF,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;IACnB,MAAM,OAAO,GACX,GAAG,YAAY,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,wCAAwC,CAAC;IAC1F,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,2BAA2B,OAAO,IAAI,CAAC,CAAC;IAC7D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { LogLevel } from "./config.js";
2
+ export interface Logger {
3
+ debug(msg: string, meta?: Record<string, unknown>): void;
4
+ info(msg: string, meta?: Record<string, unknown>): void;
5
+ warn(msg: string, meta?: Record<string, unknown>): void;
6
+ error(msg: string, meta?: Record<string, unknown>): void;
7
+ }
8
+ /**
9
+ * Minimal level-gated logger. Writes to **stderr** only — stdout is reserved for
10
+ * the MCP JSON-RPC stream. Callers must never pass spec content, API keys, JWTs,
11
+ * or request bodies as `msg`/`meta`; this logger does not attempt to parse specs
12
+ * out of arbitrary strings, so the discipline lives at the call sites.
13
+ */
14
+ export declare function createLogger(level?: LogLevel): Logger;
package/dist/logger.js ADDED
@@ -0,0 +1,25 @@
1
+ const RANK = { debug: 10, info: 20, warn: 30, error: 40 };
2
+ /**
3
+ * Minimal level-gated logger. Writes to **stderr** only — stdout is reserved for
4
+ * the MCP JSON-RPC stream. Callers must never pass spec content, API keys, JWTs,
5
+ * or request bodies as `msg`/`meta`; this logger does not attempt to parse specs
6
+ * out of arbitrary strings, so the discipline lives at the call sites.
7
+ */
8
+ export function createLogger(level = "info") {
9
+ const threshold = RANK[level];
10
+ const emit = (lvl, msg, meta) => {
11
+ if (RANK[lvl] < threshold)
12
+ return;
13
+ const line = meta
14
+ ? `[specshield-mcp] ${lvl}: ${msg} ${JSON.stringify(meta)}`
15
+ : `[specshield-mcp] ${lvl}: ${msg}`;
16
+ process.stderr.write(line + "\n");
17
+ };
18
+ return {
19
+ debug: (m, meta) => emit("debug", m, meta),
20
+ info: (m, meta) => emit("info", m, meta),
21
+ warn: (m, meta) => emit("warn", m, meta),
22
+ error: (m, meta) => emit("error", m, meta),
23
+ };
24
+ }
25
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAEA,MAAM,IAAI,GAA6B,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC;AASpF;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,QAAkB,MAAM;IACnD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC9B,MAAM,IAAI,GAAG,CAAC,GAAa,EAAE,GAAW,EAAE,IAA8B,EAAE,EAAE;QAC1E,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,SAAS;YAAE,OAAO;QAClC,MAAM,IAAI,GAAG,IAAI;YACf,CAAC,CAAC,oBAAoB,GAAG,KAAK,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE;YAC3D,CAAC,CAAC,oBAAoB,GAAG,KAAK,GAAG,EAAE,CAAC;QACtC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC,CAAC;IACF,OAAO;QACL,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC;QAC1C,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC;QACxC,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,CAAC;QACxC,KAAK,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC;KAC3C,CAAC;AACJ,CAAC"}
@@ -0,0 +1 @@
1
+ export declare const HELP_TEXT = "# SpecShield MCP tools\n\nSpecShield answers one question inside your AI coding agent: **\"Is it safe to ship\nthis API change to my consumers?\"** All tools are **read-only** \u2014 they analyze API\ncontract compatibility and never modify your code.\n\n## Tools (in order)\n\n1. **is_change_safe** \u2014 THE DEPLOY GATE. Given a base and target spec, returns\n `safeToMerge`, a risk level, and the exact blocking reasons. Use this before\n merging or releasing an API change.\n2. **explain_breaking_changes** \u2014 plain-language explanation of what breaks, the\n developer/consumer impact, and suggested migration.\n3. **generate_migration_guide** \u2014 a migration guide (markdown) + safe rollout steps.\n4. **generate_release_notes** \u2014 release notes for developer/customer/internal audiences.\n5. **compare_specs** \u2014 the raw diff (breaking / additions / modifications / warnings)\n with a risk score. Prefer is_change_safe for a go/no-go decision.\n\nEach tool accepts specs as inline content (`baseSpecContent`/`targetSpecContent`)\nor file paths (`baseSpecPath`/`targetSpecPath`).\n\n## Planned\n\n- **run_governance_review** \u2014 deterministic API governance ruleset. Ships once the\n backend `/api/governance` endpoint lands (a later release).\n\n## Privacy\n\nRequires a SpecShield API key (`SPECSHIELD_API_KEY`). Specs are sent to your\nconfigured SpecShield backend for analysis. This server never logs spec content or\nAPI keys.\n";
@@ -0,0 +1,33 @@
1
+ export const HELP_TEXT = `# SpecShield MCP tools
2
+
3
+ SpecShield answers one question inside your AI coding agent: **"Is it safe to ship
4
+ this API change to my consumers?"** All tools are **read-only** — they analyze API
5
+ contract compatibility and never modify your code.
6
+
7
+ ## Tools (in order)
8
+
9
+ 1. **is_change_safe** — THE DEPLOY GATE. Given a base and target spec, returns
10
+ \`safeToMerge\`, a risk level, and the exact blocking reasons. Use this before
11
+ merging or releasing an API change.
12
+ 2. **explain_breaking_changes** — plain-language explanation of what breaks, the
13
+ developer/consumer impact, and suggested migration.
14
+ 3. **generate_migration_guide** — a migration guide (markdown) + safe rollout steps.
15
+ 4. **generate_release_notes** — release notes for developer/customer/internal audiences.
16
+ 5. **compare_specs** — the raw diff (breaking / additions / modifications / warnings)
17
+ with a risk score. Prefer is_change_safe for a go/no-go decision.
18
+
19
+ Each tool accepts specs as inline content (\`baseSpecContent\`/\`targetSpecContent\`)
20
+ or file paths (\`baseSpecPath\`/\`targetSpecPath\`).
21
+
22
+ ## Planned
23
+
24
+ - **run_governance_review** — deterministic API governance ruleset. Ships once the
25
+ backend \`/api/governance\` endpoint lands (a later release).
26
+
27
+ ## Privacy
28
+
29
+ Requires a SpecShield API key (\`SPECSHIELD_API_KEY\`). Specs are sent to your
30
+ configured SpecShield backend for analysis. This server never logs spec content or
31
+ API keys.
32
+ `;
33
+ //# sourceMappingURL=help.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"help.js","sourceRoot":"","sources":["../../src/resources/help.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA+BxB,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import type { SpecShieldApiClient } from "./client.js";
3
+ import type { Logger } from "./logger.js";
4
+ export declare const SERVER_NAME = "specshield-mcp-server";
5
+ export declare const SERVER_VERSION = "0.1.0";
6
+ export interface ServerDeps {
7
+ client: SpecShieldApiClient;
8
+ logger: Logger;
9
+ }
10
+ /**
11
+ * Builds the MCP server with all tools registered in pitch order and the single
12
+ * read-only help resource. `deps.client` is injectable so tests can mock the
13
+ * backend without any network I/O.
14
+ */
15
+ export declare function createServer(deps: ServerDeps): McpServer;
package/dist/server.js ADDED
@@ -0,0 +1,31 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { TOOLS } from "./tools/index.js";
3
+ import { HELP_TEXT } from "./resources/help.js";
4
+ export const SERVER_NAME = "specshield-mcp-server";
5
+ export const SERVER_VERSION = "0.1.0";
6
+ /**
7
+ * Builds the MCP server with all tools registered in pitch order and the single
8
+ * read-only help resource. `deps.client` is injectable so tests can mock the
9
+ * backend without any network I/O.
10
+ */
11
+ export function createServer(deps) {
12
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
13
+ const ctx = { client: deps.client, logger: deps.logger };
14
+ for (const tool of TOOLS) {
15
+ server.registerTool(tool.name, {
16
+ title: tool.title,
17
+ description: tool.description,
18
+ inputSchema: tool.inputSchema,
19
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
20
+ }, (args) => tool.handler(args, ctx));
21
+ }
22
+ server.registerResource("help", "specshield://help/tools", {
23
+ title: "SpecShield MCP tools",
24
+ description: "How to use the SpecShield deploy-gate tools.",
25
+ mimeType: "text/markdown",
26
+ }, async (uri) => ({
27
+ contents: [{ uri: uri.href, mimeType: "text/markdown", text: HELP_TEXT }],
28
+ }));
29
+ return server;
30
+ }
31
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAIpE,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEhD,MAAM,CAAC,MAAM,WAAW,GAAG,uBAAuB,CAAC;AACnD,MAAM,CAAC,MAAM,cAAc,GAAG,OAAO,CAAC;AAOtC;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAgB;IAC3C,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;IAC7E,MAAM,GAAG,GAAgB,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IAEtE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,CAAC,YAAY,CACjB,IAAI,CAAC,IAAI,EACT;YACE,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,WAAW,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE;SACjF,EACD,CAAC,IAA6B,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAC3D,CAAC;IACJ,CAAC;IAED,MAAM,CAAC,gBAAgB,CACrB,MAAM,EACN,yBAAyB,EACzB;QACE,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EAAE,8CAA8C;QAC3D,QAAQ,EAAE,eAAe;KAC1B,EACD,KAAK,EAAE,GAAQ,EAAE,EAAE,CAAC,CAAC;QACnB,QAAQ,EAAE,CAAC,EAAE,GAAG,EAAE,GAAG,CAAC,IAAI,EAAE,QAAQ,EAAE,eAAe,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;KAC1E,CAAC,CACH,CAAC;IAEF,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,11 @@
1
+ export interface SpecSource {
2
+ content?: string;
3
+ path?: string;
4
+ }
5
+ /**
6
+ * Resolves a spec to its text content from either inline content or a file path.
7
+ * A path is only read when the user explicitly passed one through the MCP client.
8
+ * Error messages never include the spec content (and only a generic note about
9
+ * the path, not the file body).
10
+ */
11
+ export declare function resolveSpec(source: SpecSource, label: string): Promise<string>;
@@ -0,0 +1,23 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { SpecShieldError } from "./errors.js";
3
+ /**
4
+ * Resolves a spec to its text content from either inline content or a file path.
5
+ * A path is only read when the user explicitly passed one through the MCP client.
6
+ * Error messages never include the spec content (and only a generic note about
7
+ * the path, not the file body).
8
+ */
9
+ export async function resolveSpec(source, label) {
10
+ if (source.content && source.content.trim().length > 0) {
11
+ return source.content;
12
+ }
13
+ if (source.path && source.path.trim().length > 0) {
14
+ try {
15
+ return await readFile(source.path, "utf8");
16
+ }
17
+ catch {
18
+ throw new SpecShieldError("validation_error", `Could not read the ${label} spec from the provided file path.`);
19
+ }
20
+ }
21
+ throw new SpecShieldError("validation_error", `Provide either ${label}SpecContent or ${label}SpecPath.`);
22
+ }
23
+ //# sourceMappingURL=specResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"specResolver.js","sourceRoot":"","sources":["../src/specResolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAO9C;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,MAAkB,EAAE,KAAa;IACjE,IAAI,MAAM,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,MAAM,CAAC,OAAO,CAAC;IACxB,CAAC;IACD,IAAI,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACjD,IAAI,CAAC;YACH,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAC7C,CAAC;QAAC,MAAM,CAAC;YACP,MAAM,IAAI,eAAe,CACvB,kBAAkB,EAClB,sBAAsB,KAAK,oCAAoC,CAChE,CAAC;QACJ,CAAC;IACH,CAAC;IACD,MAAM,IAAI,eAAe,CACvB,kBAAkB,EAClB,kBAAkB,KAAK,kBAAkB,KAAK,WAAW,CAC1D,CAAC;AACJ,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { type ToolDef } from "./shared.js";
2
+ /**
3
+ * Table-stakes raw diff — registered LAST. The differentiated value is the
4
+ * deploy gate (is_change_safe); this is here for completeness.
5
+ */
6
+ export declare const compareSpecs: ToolDef;
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { guarded, resolveBothSpecs, specSourceShape, textResult } from "./shared.js";
3
+ /**
4
+ * Table-stakes raw diff — registered LAST. The differentiated value is the
5
+ * deploy gate (is_change_safe); this is here for completeness.
6
+ */
7
+ export const compareSpecs = {
8
+ name: "compare_specs",
9
+ title: "Compare two API specs",
10
+ description: "Compares two API specs and lists breaking changes, additions, modifications, and warnings " +
11
+ "with a risk score and compatibility summary. For a merge/deploy decision prefer " +
12
+ "is_change_safe. Analyzes API contract compatibility only; it does NOT modify code.",
13
+ inputSchema: {
14
+ ...specSourceShape,
15
+ specFormat: z.enum(["openapi", "pact"]).optional(),
16
+ failOnBreaking: z
17
+ .boolean()
18
+ .optional()
19
+ .describe("If true, the header flags when any breaking change is present."),
20
+ },
21
+ handler: guarded(async (args, ctx) => {
22
+ const { baseSpec, targetSpec } = await resolveBothSpecs(args);
23
+ const res = await ctx.client.post("/api/intelligence/compare", { baseSpec, targetSpec, specFormat: args.specFormat });
24
+ const breakingCount = res.breakingChanges?.length ?? 0;
25
+ const header = args.failOnBreaking && breakingCount > 0
26
+ ? `⛔ ${breakingCount} breaking change(s) (failOnBreaking) — risk ${res.risk.level}`
27
+ : `risk ${res.risk.level} · ${breakingCount} breaking · ${res.additions?.length ?? 0} additions`;
28
+ return textResult(header, res);
29
+ }),
30
+ };
31
+ //# sourceMappingURL=compareSpecs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compareSpecs.js","sourceRoot":"","sources":["../../src/tools/compareSpecs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,UAAU,EAAgB,MAAM,aAAa,CAAC;AAEnG;;;GAGG;AACH,MAAM,CAAC,MAAM,YAAY,GAAY;IACnC,IAAI,EAAE,eAAe;IACrB,KAAK,EAAE,uBAAuB;IAC9B,WAAW,EACT,4FAA4F;QAC5F,kFAAkF;QAClF,oFAAoF;IACtF,WAAW,EAAE;QACX,GAAG,eAAe;QAClB,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE;QAClD,cAAc,EAAE,CAAC;aACd,OAAO,EAAE;aACT,QAAQ,EAAE;aACV,QAAQ,CAAC,gEAAgE,CAAC;KAC9E;IACD,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QACnC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAC/B,2BAA2B,EAC3B,EAAE,QAAQ,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,CACtD,CAAC;QACF,MAAM,aAAa,GAAG,GAAG,CAAC,eAAe,EAAE,MAAM,IAAI,CAAC,CAAC;QACvD,MAAM,MAAM,GACV,IAAI,CAAC,cAAc,IAAI,aAAa,GAAG,CAAC;YACtC,CAAC,CAAC,KAAK,aAAa,+CAA+C,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE;YACnF,CAAC,CAAC,QAAQ,GAAG,CAAC,IAAI,CAAC,KAAK,MAAM,aAAa,eAAe,GAAG,CAAC,SAAS,EAAE,MAAM,IAAI,CAAC,YAAY,CAAC;QACrG,OAAO,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,CAAC,CAAC;CACH,CAAC"}
@@ -0,0 +1,8 @@
1
+ import { type ToolDef } from "./shared.js";
2
+ /**
3
+ * Explains the breaking changes in plain language. Deterministic in the MVP: it
4
+ * sources the changes and the migration steps entirely from the backend
5
+ * (/compare + /migration-guide-preview) and only reshapes them — no diff or risk
6
+ * logic lives here. AI-enhanced explanations arrive after Phase 4.
7
+ */
8
+ export declare const explainBreakingChanges: ToolDef;
@@ -0,0 +1,37 @@
1
+ import { consumerImpact, guarded, resolveBothSpecs, specSourceShape, textResult, } from "./shared.js";
2
+ /**
3
+ * Explains the breaking changes in plain language. Deterministic in the MVP: it
4
+ * sources the changes and the migration steps entirely from the backend
5
+ * (/compare + /migration-guide-preview) and only reshapes them — no diff or risk
6
+ * logic lives here. AI-enhanced explanations arrive after Phase 4.
7
+ */
8
+ export const explainBreakingChanges = {
9
+ name: "explain_breaking_changes",
10
+ title: "Explain the breaking changes",
11
+ description: "Explains what breaks in an API spec change and why, with developer impact, likely consumer " +
12
+ "impact, and suggested migration steps. Analyzes API contract compatibility only; it does " +
13
+ "NOT modify code.",
14
+ inputSchema: { ...specSourceShape },
15
+ handler: guarded(async (args, ctx) => {
16
+ const { baseSpec, targetSpec } = await resolveBothSpecs(args);
17
+ const cmp = await ctx.client.post("/api/intelligence/compare", { baseSpec, targetSpec });
18
+ const migration = await ctx.client.post("/api/intelligence/migration-guide-preview", { baseSpec, targetSpec, language: "generic" });
19
+ const breaking = cmp.breakingChanges ?? [];
20
+ const result = {
21
+ explanation: breaking.length > 0
22
+ ? `${breaking.length} breaking change(s) detected between the two specs.`
23
+ : "No breaking changes detected.",
24
+ breakingChanges: breaking.map((c) => ({
25
+ change: c.type,
26
+ where: `${(c.method ?? "").toUpperCase()} ${c.path ?? ""}`.trim(),
27
+ detail: c.description,
28
+ })),
29
+ developerImpact: cmp.risk.summary,
30
+ likelyConsumerImpact: consumerImpact(cmp.risk.level),
31
+ suggestedMigration: migration.steps,
32
+ };
33
+ const header = breaking.length > 0 ? `⛔ ${breaking.length} breaking change(s)` : "✅ No breaking changes";
34
+ return textResult(header, result);
35
+ }),
36
+ };
37
+ //# sourceMappingURL=explainBreakingChanges.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"explainBreakingChanges.js","sourceRoot":"","sources":["../../src/tools/explainBreakingChanges.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,OAAO,EACP,gBAAgB,EAChB,eAAe,EACf,UAAU,GAEX,MAAM,aAAa,CAAC;AAErB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAY;IAC7C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,8BAA8B;IACrC,WAAW,EACT,6FAA6F;QAC7F,2FAA2F;QAC3F,kBAAkB;IACpB,WAAW,EAAE,EAAE,GAAG,eAAe,EAAE;IACnC,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QACnC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAC/B,2BAA2B,EAC3B,EAAE,QAAQ,EAAE,UAAU,EAAE,CACzB,CAAC;QACF,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CACrC,2CAA2C,EAC3C,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,SAAS,EAAE,CAC9C,CAAC;QAEF,MAAM,QAAQ,GAAG,GAAG,CAAC,eAAe,IAAI,EAAE,CAAC;QAC3C,MAAM,MAAM,GAAG;YACb,WAAW,EACT,QAAQ,CAAC,MAAM,GAAG,CAAC;gBACjB,CAAC,CAAC,GAAG,QAAQ,CAAC,MAAM,qDAAqD;gBACzE,CAAC,CAAC,+BAA+B;YACrC,eAAe,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBACpC,MAAM,EAAE,CAAC,CAAC,IAAI;gBACd,KAAK,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE;gBACjE,MAAM,EAAE,CAAC,CAAC,WAAW;aACtB,CAAC,CAAC;YACH,eAAe,EAAE,GAAG,CAAC,IAAI,CAAC,OAAO;YACjC,oBAAoB,EAAE,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;YACpD,kBAAkB,EAAE,SAAS,CAAC,KAAK;SACpC,CAAC;QACF,MAAM,MAAM,GACV,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,QAAQ,CAAC,MAAM,qBAAqB,CAAC,CAAC,CAAC,uBAAuB,CAAC;QAC5F,OAAO,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,CAAC,CAAC;CACH,CAAC"}
@@ -0,0 +1,6 @@
1
+ import { type ToolDef } from "./shared.js";
2
+ /**
3
+ * Deterministic migration guide, sourced entirely from the backend
4
+ * (/migration-guide-preview). No migration logic lives here.
5
+ */
6
+ export declare const generateMigrationGuide: ToolDef;
@@ -0,0 +1,36 @@
1
+ import { z } from "zod";
2
+ import { guarded, markdownResult, resolveBothSpecs, specSourceShape } from "./shared.js";
3
+ /**
4
+ * Deterministic migration guide, sourced entirely from the backend
5
+ * (/migration-guide-preview). No migration logic lives here.
6
+ */
7
+ export const generateMigrationGuide = {
8
+ name: "generate_migration_guide",
9
+ title: "Generate a migration guide",
10
+ description: "Generates a migration guide (markdown) and safe rollout steps for the breaking changes in " +
11
+ "an API spec change. Analyzes API contract compatibility only; it does NOT modify code.",
12
+ inputSchema: {
13
+ ...specSourceShape,
14
+ language: z.enum(["java", "node", "python", "go", "curl", "generic"]).optional(),
15
+ },
16
+ handler: guarded(async (args, ctx) => {
17
+ const { baseSpec, targetSpec } = await resolveBothSpecs(args);
18
+ const advice = await ctx.client.post("/api/intelligence/migration-guide-preview", { baseSpec, targetSpec, language: args.language ?? "generic" });
19
+ const lines = [
20
+ `# Migration guide (${advice.language})`,
21
+ "",
22
+ advice.summary,
23
+ "",
24
+ ...(advice.steps.length > 0
25
+ ? ["## Steps", ...advice.steps.map((s, i) => `${i + 1}. ${s}`)]
26
+ : ["_No breaking changes — no migration required._"]),
27
+ "",
28
+ "## Safe rollout",
29
+ "1. Ship the change behind a new API version where possible.",
30
+ "2. Announce the change and give consumers a migration window.",
31
+ "3. Monitor error rates on affected endpoints after release.",
32
+ ];
33
+ return markdownResult(lines.join("\n"));
34
+ }),
35
+ };
36
+ //# sourceMappingURL=generateMigrationGuide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateMigrationGuide.js","sourceRoot":"","sources":["../../src/tools/generateMigrationGuide.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,eAAe,EAAgB,MAAM,aAAa,CAAC;AAEvG;;;GAGG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAY;IAC7C,IAAI,EAAE,0BAA0B;IAChC,KAAK,EAAE,4BAA4B;IACnC,WAAW,EACT,4FAA4F;QAC5F,wFAAwF;IAC1F,WAAW,EAAE;QACX,GAAG,eAAe;QAClB,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,EAAE;KACjF;IACD,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QACnC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAClC,2CAA2C,EAC3C,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,SAAS,EAAE,CAC/D,CAAC;QAEF,MAAM,KAAK,GAAG;YACZ,sBAAsB,MAAM,CAAC,QAAQ,GAAG;YACxC,EAAE;YACF,MAAM,CAAC,OAAO;YACd,EAAE;YACF,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC;gBACzB,CAAC,CAAC,CAAC,UAAU,EAAE,GAAG,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;gBAC/D,CAAC,CAAC,CAAC,gDAAgD,CAAC,CAAC;YACvD,EAAE;YACF,iBAAiB;YACjB,6DAA6D;YAC7D,+DAA+D;YAC/D,6DAA6D;SAC9D,CAAC;QACF,OAAO,cAAc,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1C,CAAC,CAAC;CACH,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { type ToolDef } from "./shared.js";
2
+ /**
3
+ * Deterministic release notes, sourced from the backend (/release-notes-preview).
4
+ */
5
+ export declare const generateReleaseNotes: ToolDef;
@@ -0,0 +1,22 @@
1
+ import { z } from "zod";
2
+ import { guarded, markdownResult, resolveBothSpecs, specSourceShape } from "./shared.js";
3
+ /**
4
+ * Deterministic release notes, sourced from the backend (/release-notes-preview).
5
+ */
6
+ export const generateReleaseNotes = {
7
+ name: "generate_release_notes",
8
+ title: "Generate release notes",
9
+ description: "Generates release notes (markdown) for an API spec change, grouped into breaking changes, " +
10
+ "additions, and other changes, tailored to an audience. Analyzes API contract compatibility " +
11
+ "only; it does NOT modify code.",
12
+ inputSchema: {
13
+ ...specSourceShape,
14
+ audience: z.enum(["developer", "customer", "internal"]).optional(),
15
+ },
16
+ handler: guarded(async (args, ctx) => {
17
+ const { baseSpec, targetSpec } = await resolveBothSpecs(args);
18
+ const notes = await ctx.client.post("/api/intelligence/release-notes-preview", { baseSpec, targetSpec, audience: args.audience ?? "developer" });
19
+ return markdownResult(notes.markdown);
20
+ }),
21
+ };
22
+ //# sourceMappingURL=generateReleaseNotes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generateReleaseNotes.js","sourceRoot":"","sources":["../../src/tools/generateReleaseNotes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,eAAe,EAAgB,MAAM,aAAa,CAAC;AAEvG;;GAEG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAY;IAC3C,IAAI,EAAE,wBAAwB;IAC9B,KAAK,EAAE,wBAAwB;IAC/B,WAAW,EACT,4FAA4F;QAC5F,6FAA6F;QAC7F,gCAAgC;IAClC,WAAW,EAAE;QACX,GAAG,eAAe;QAClB,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC,CAAC,QAAQ,EAAE;KACnE;IACD,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QACnC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,KAAK,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CACjC,yCAAyC,EACzC,EAAE,QAAQ,EAAE,UAAU,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,WAAW,EAAE,CACjE,CAAC;QACF,OAAO,cAAc,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IACxC,CAAC,CAAC;CACH,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { ToolDef } from "./shared.js";
2
+ /**
3
+ * Registration order IS the pitch: the differentiated deploy gate
4
+ * (is_change_safe) is first; the table-stakes raw diff (compare_specs) is last.
5
+ *
6
+ * `run_governance_review` is intentionally NOT registered in the MVP: there is
7
+ * no backend governance endpoint yet (it ships with /api/governance in Phase 4),
8
+ * and every registered tool must call the backend rather than fabricate results.
9
+ */
10
+ export declare const TOOLS: ToolDef[];
@@ -0,0 +1,21 @@
1
+ import { isChangeSafe } from "./isChangeSafe.js";
2
+ import { explainBreakingChanges } from "./explainBreakingChanges.js";
3
+ import { generateMigrationGuide } from "./generateMigrationGuide.js";
4
+ import { generateReleaseNotes } from "./generateReleaseNotes.js";
5
+ import { compareSpecs } from "./compareSpecs.js";
6
+ /**
7
+ * Registration order IS the pitch: the differentiated deploy gate
8
+ * (is_change_safe) is first; the table-stakes raw diff (compare_specs) is last.
9
+ *
10
+ * `run_governance_review` is intentionally NOT registered in the MVP: there is
11
+ * no backend governance endpoint yet (it ships with /api/governance in Phase 4),
12
+ * and every registered tool must call the backend rather than fabricate results.
13
+ */
14
+ export const TOOLS = [
15
+ isChangeSafe,
16
+ explainBreakingChanges,
17
+ generateMigrationGuide,
18
+ generateReleaseNotes,
19
+ compareSpecs,
20
+ ];
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tools/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAGjD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,KAAK,GAAc;IAC9B,YAAY;IACZ,sBAAsB;IACtB,sBAAsB;IACtB,oBAAoB;IACpB,YAAY;CACb,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { type ToolDef } from "./shared.js";
2
+ /**
3
+ * THE HERO TOOL — the deploy gate. Registered first.
4
+ */
5
+ export declare const isChangeSafe: ToolDef;
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { guarded, resolveBothSpecs, specSourceShape, textResult } from "./shared.js";
3
+ /**
4
+ * THE HERO TOOL — the deploy gate. Registered first.
5
+ */
6
+ export const isChangeSafe = {
7
+ name: "is_change_safe",
8
+ title: "Is this API change safe to ship?",
9
+ description: "THE DEPLOY GATE. Analyzes whether an API spec change is safe to merge/deploy — will it " +
10
+ "break existing consumers? Returns a safeToMerge verdict, a risk level, the exact blocking " +
11
+ "reasons, and a recommended action. Use this before merging or releasing an API change to " +
12
+ "catch breaking changes before they reach your consumers. Analyzes API contract " +
13
+ "compatibility only; it does NOT modify code.",
14
+ inputSchema: {
15
+ ...specSourceShape,
16
+ specFormat: z.enum(["openapi", "pact"]).optional(),
17
+ environment: z
18
+ .string()
19
+ .optional()
20
+ .describe("Optional target environment, e.g. staging or production."),
21
+ },
22
+ handler: guarded(async (args, ctx) => {
23
+ const { baseSpec, targetSpec } = await resolveBothSpecs(args);
24
+ const res = await ctx.client.post("/api/intelligence/change-safety", { baseSpec, targetSpec, environment: args.environment });
25
+ const header = res.safeToMerge
26
+ ? "✅ SAFE TO MERGE"
27
+ : `⛔ NOT SAFE TO MERGE — risk ${res.riskLevel}`;
28
+ return textResult(header, res);
29
+ }),
30
+ };
31
+ //# sourceMappingURL=isChangeSafe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"isChangeSafe.js","sourceRoot":"","sources":["../../src/tools/isChangeSafe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,UAAU,EAAgB,MAAM,aAAa,CAAC;AAEnG;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAY;IACnC,IAAI,EAAE,gBAAgB;IACtB,KAAK,EAAE,kCAAkC;IACzC,WAAW,EACT,yFAAyF;QACzF,4FAA4F;QAC5F,2FAA2F;QAC3F,iFAAiF;QACjF,8CAA8C;IAChD,WAAW,EAAE;QACX,GAAG,eAAe;QAClB,UAAU,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC,QAAQ,EAAE;QAClD,WAAW,EAAE,CAAC;aACX,MAAM,EAAE;aACR,QAAQ,EAAE;aACV,QAAQ,CAAC,0DAA0D,CAAC;KACxE;IACD,OAAO,EAAE,OAAO,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,EAAE,EAAE;QACnC,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,MAAM,gBAAgB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,IAAI,CAC/B,iCAAiC,EACjC,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CACxD,CAAC;QACF,MAAM,MAAM,GAAG,GAAG,CAAC,WAAW;YAC5B,CAAC,CAAC,iBAAiB;YACnB,CAAC,CAAC,8BAA8B,GAAG,CAAC,SAAS,EAAE,CAAC;QAClD,OAAO,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,CAAC,CAAC;CACH,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
3
+ import type { SpecShieldApiClient } from "../client.js";
4
+ import type { Logger } from "../logger.js";
5
+ import type { RiskLevel } from "../backendTypes.js";
6
+ export interface ToolContext {
7
+ client: SpecShieldApiClient;
8
+ logger: Logger;
9
+ }
10
+ export interface ToolDef {
11
+ name: string;
12
+ title: string;
13
+ description: string;
14
+ inputSchema: z.ZodRawShape;
15
+ handler: (args: Record<string, unknown>, ctx: ToolContext) => Promise<CallToolResult>;
16
+ }
17
+ /** base/target spec inputs shared by every tool (inline content or a file path). */
18
+ export declare const specSourceShape: {
19
+ baseSpecContent: z.ZodOptional<z.ZodString>;
20
+ baseSpecPath: z.ZodOptional<z.ZodString>;
21
+ targetSpecContent: z.ZodOptional<z.ZodString>;
22
+ targetSpecPath: z.ZodOptional<z.ZodString>;
23
+ };
24
+ export declare function resolveBothSpecs(args: Record<string, unknown>): Promise<{
25
+ baseSpec: string;
26
+ targetSpec: string;
27
+ }>;
28
+ export declare function textResult(header: string, data: unknown): CallToolResult;
29
+ export declare function markdownResult(markdown: string): CallToolResult;
30
+ export declare function errorResult(err: unknown): CallToolResult;
31
+ /** Wrap a handler so any error becomes a safe, machine-readable isError result. */
32
+ export declare function guarded(fn: (args: Record<string, unknown>, ctx: ToolContext) => Promise<CallToolResult>): ToolDef["handler"];
33
+ export declare function consumerImpact(level: RiskLevel): string;
@@ -0,0 +1,57 @@
1
+ import { z } from "zod";
2
+ import { SpecShieldError } from "../errors.js";
3
+ import { resolveSpec } from "../specResolver.js";
4
+ /** base/target spec inputs shared by every tool (inline content or a file path). */
5
+ export const specSourceShape = {
6
+ baseSpecContent: z.string().optional().describe("Inline base (old) spec content."),
7
+ baseSpecPath: z.string().optional().describe("Path to the base (old) spec file."),
8
+ targetSpecContent: z.string().optional().describe("Inline target (new) spec content."),
9
+ targetSpecPath: z.string().optional().describe("Path to the target (new) spec file."),
10
+ };
11
+ export async function resolveBothSpecs(args) {
12
+ const baseSpec = await resolveSpec({ content: args.baseSpecContent, path: args.baseSpecPath }, "base");
13
+ const targetSpec = await resolveSpec({ content: args.targetSpecContent, path: args.targetSpecPath }, "target");
14
+ return { baseSpec, targetSpec };
15
+ }
16
+ export function textResult(header, data) {
17
+ const body = JSON.stringify(data, null, 2);
18
+ return { content: [{ type: "text", text: header ? `${header}\n\n${body}` : body }] };
19
+ }
20
+ export function markdownResult(markdown) {
21
+ return { content: [{ type: "text", text: markdown }] };
22
+ }
23
+ export function errorResult(err) {
24
+ const e = err instanceof SpecShieldError
25
+ ? err
26
+ : new SpecShieldError("backend_error", "Unexpected error while contacting SpecShield.");
27
+ return { isError: true, content: [{ type: "text", text: JSON.stringify(e.toPayload(), null, 2) }] };
28
+ }
29
+ /** Wrap a handler so any error becomes a safe, machine-readable isError result. */
30
+ export function guarded(fn) {
31
+ return async (args, ctx) => {
32
+ try {
33
+ return await fn(args, ctx);
34
+ }
35
+ catch (err) {
36
+ // Log the code only — never the message body, spec, or key.
37
+ ctx.logger.warn("tool call failed", {
38
+ code: err instanceof SpecShieldError ? err.code : "unknown",
39
+ });
40
+ return errorResult(err);
41
+ }
42
+ };
43
+ }
44
+ export function consumerImpact(level) {
45
+ switch (level) {
46
+ case "CRITICAL":
47
+ return "Existing consumers will break until they are updated.";
48
+ case "HIGH":
49
+ return "Existing consumers are likely to break; coordinate an upgrade.";
50
+ case "MEDIUM":
51
+ return "No breaking changes, but consumers may see behavior differences.";
52
+ case "LOW":
53
+ default:
54
+ return "Additive/non-breaking; existing consumers are unaffected.";
55
+ }
56
+ }
57
+ //# sourceMappingURL=shared.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shared.js","sourceRoot":"","sources":["../../src/tools/shared.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "specshield-mcp-server",
3
+ "version": "0.1.0",
4
+ "description": "SpecShield MCP server — the API-change deploy gate for AI coding agents. Ask 'is it safe to ship this API change to my consumers?' inside Claude, Cursor, and other MCP clients. Analyze-only; it never modifies code.",
5
+ "license": "MIT",
6
+ "author": "SpecShield Software Private Limited",
7
+ "homepage": "https://specshield.io",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/specshield26/specshield-mcp-server.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/specshield26/specshield-mcp-server/issues"
14
+ },
15
+ "type": "module",
16
+ "engines": {
17
+ "node": ">=20.0.0"
18
+ },
19
+ "bin": {
20
+ "specshield-mcp": "dist/index.js"
21
+ },
22
+ "main": "dist/index.js",
23
+ "types": "dist/index.d.ts",
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "LICENSE"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "start": "node dist/index.js",
32
+ "dev": "tsx src/index.ts",
33
+ "test": "vitest run",
34
+ "test:watch": "vitest",
35
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
36
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
37
+ "smoke": "node scripts/smoke.mjs",
38
+ "prepublishOnly": "npm run build"
39
+ },
40
+ "keywords": [
41
+ "mcp",
42
+ "model-context-protocol",
43
+ "openapi",
44
+ "api",
45
+ "breaking-changes",
46
+ "contract-compatibility-testing",
47
+ "can-i-deploy",
48
+ "deploy-gate",
49
+ "specshield",
50
+ "claude",
51
+ "cursor"
52
+ ],
53
+ "dependencies": {
54
+ "@modelcontextprotocol/sdk": "^1.29.0",
55
+ "zod": "^3.23.8"
56
+ },
57
+ "devDependencies": {
58
+ "@eslint/js": "^9.7.0",
59
+ "@types/node": "^20.14.0",
60
+ "eslint": "^9.7.0",
61
+ "prettier": "^3.3.0",
62
+ "tsx": "^4.16.0",
63
+ "typescript": "^5.5.0",
64
+ "typescript-eslint": "^8.0.0",
65
+ "vitest": "^2.0.0"
66
+ }
67
+ }