@dawmatt/api-grade-mcp 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 (70) hide show
  1. package/README.md +128 -0
  2. package/dist/auth/entra.d.ts +8 -0
  3. package/dist/auth/entra.d.ts.map +1 -0
  4. package/dist/auth/entra.js +59 -0
  5. package/dist/auth/entra.js.map +1 -0
  6. package/dist/auth/github.d.ts +10 -0
  7. package/dist/auth/github.d.ts.map +1 -0
  8. package/dist/auth/github.js +42 -0
  9. package/dist/auth/github.js.map +1 -0
  10. package/dist/config/resolve-ruleset.d.ts +3 -0
  11. package/dist/config/resolve-ruleset.d.ts.map +1 -0
  12. package/dist/config/resolve-ruleset.js +31 -0
  13. package/dist/config/resolve-ruleset.js.map +1 -0
  14. package/dist/config/ruleset-config.d.ts +13 -0
  15. package/dist/config/ruleset-config.d.ts.map +1 -0
  16. package/dist/config/ruleset-config.js +49 -0
  17. package/dist/config/ruleset-config.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +6 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/server.d.ts +3 -0
  23. package/dist/server.d.ts.map +1 -0
  24. package/dist/server.js +33 -0
  25. package/dist/server.js.map +1 -0
  26. package/dist/tools/assert-grade.d.ts +4 -0
  27. package/dist/tools/assert-grade.d.ts.map +1 -0
  28. package/dist/tools/assert-grade.js +132 -0
  29. package/dist/tools/assert-grade.js.map +1 -0
  30. package/dist/tools/configure-ruleset.d.ts +4 -0
  31. package/dist/tools/configure-ruleset.d.ts.map +1 -0
  32. package/dist/tools/configure-ruleset.js +94 -0
  33. package/dist/tools/configure-ruleset.js.map +1 -0
  34. package/dist/tools/get-ruleset-config.d.ts +4 -0
  35. package/dist/tools/get-ruleset-config.d.ts.map +1 -0
  36. package/dist/tools/get-ruleset-config.js +53 -0
  37. package/dist/tools/get-ruleset-config.js.map +1 -0
  38. package/dist/tools/grade-detailed.d.ts +4 -0
  39. package/dist/tools/grade-detailed.d.ts.map +1 -0
  40. package/dist/tools/grade-detailed.js +143 -0
  41. package/dist/tools/grade-detailed.js.map +1 -0
  42. package/dist/tools/grade.d.ts +4 -0
  43. package/dist/tools/grade.d.ts.map +1 -0
  44. package/dist/tools/grade.js +134 -0
  45. package/dist/tools/grade.js.map +1 -0
  46. package/dist/tools/non-breaking.d.ts +4 -0
  47. package/dist/tools/non-breaking.d.ts.map +1 -0
  48. package/dist/tools/non-breaking.js +138 -0
  49. package/dist/tools/non-breaking.js.map +1 -0
  50. package/dist/tools/quick-fixes-only.d.ts +4 -0
  51. package/dist/tools/quick-fixes-only.d.ts.map +1 -0
  52. package/dist/tools/quick-fixes-only.js +138 -0
  53. package/dist/tools/quick-fixes-only.js.map +1 -0
  54. package/dist/tools/set-ruleset-config.d.ts +4 -0
  55. package/dist/tools/set-ruleset-config.d.ts.map +1 -0
  56. package/dist/tools/set-ruleset-config.js +94 -0
  57. package/dist/tools/set-ruleset-config.js.map +1 -0
  58. package/dist/types.d.ts +27 -0
  59. package/dist/types.d.ts.map +1 -0
  60. package/dist/types.js +2 -0
  61. package/dist/types.js.map +1 -0
  62. package/dist/utils/classify.d.ts +14 -0
  63. package/dist/utils/classify.d.ts.map +1 -0
  64. package/dist/utils/classify.js +123 -0
  65. package/dist/utils/classify.js.map +1 -0
  66. package/dist/utils/errors.d.ts +33 -0
  67. package/dist/utils/errors.d.ts.map +1 -0
  68. package/dist/utils/errors.js +56 -0
  69. package/dist/utils/errors.js.map +1 -0
  70. package/package.json +61 -0
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @dawmatt/api-grade-mcp
2
+
3
+ MCP (Model Context Protocol) server that exposes api-grade capabilities as six AI tools — grade OpenAPI and AsyncAPI specifications directly from Claude Code, GitHub Copilot, or any MCP-compatible AI host.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @dawmatt/api-grade-mcp
9
+ ```
10
+
11
+ Or use without installing (recommended):
12
+
13
+ ```bash
14
+ npx -y @dawmatt/api-grade-mcp
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### Claude Code
20
+
21
+ ```bash
22
+ claude mcp add api-grade -- npx -y @dawmatt/api-grade-mcp
23
+ ```
24
+
25
+ ### Claude Desktop
26
+
27
+ Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
28
+
29
+ ```json
30
+ {
31
+ "mcpServers": {
32
+ "api-grade": {
33
+ "command": "npx",
34
+ "args": ["-y", "@dawmatt/api-grade-mcp"]
35
+ }
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### GitHub Copilot (VS Code Agent mode)
41
+
42
+ Create `.vscode/mcp.json` in your project root:
43
+
44
+ ```json
45
+ {
46
+ "servers": {
47
+ "api-grade": {
48
+ "type": "stdio",
49
+ "command": "npx",
50
+ "args": ["-y", "@dawmatt/api-grade-mcp"]
51
+ }
52
+ }
53
+ }
54
+ ```
55
+
56
+ ## Available Tools
57
+
58
+ | Tool | Description |
59
+ |------|-------------|
60
+ | `grade-api` | Letter grade, score, and summary — token-efficient overview |
61
+ | `grade-api-detailed` | Full grade with all violations and diagnostics |
62
+ | `assert-api-grade` | Pass/fail assertion for a minimum grade threshold |
63
+ | `grade-api-quick-fixes-only` | Classified list of quick fixes (safe, non-breaking improvements) for AI-assisted correction |
64
+ | `set-ruleset-config` | Set the default Spectral ruleset at session, workspace, or global scope |
65
+ | `get-ruleset-config` | Get the active Spectral ruleset and which scope is effective |
66
+
67
+ ## Usage Examples
68
+
69
+ Once configured, ask your AI tool naturally:
70
+
71
+ ```
72
+ Grade the API at /workspace/my-api/openapi.yaml
73
+ ```
74
+
75
+ ```
76
+ Check whether /workspace/my-api/openapi.yaml meets a minimum grade of B
77
+ ```
78
+
79
+ ```
80
+ Apply quick fixes to /workspace/my-api/openapi.yaml
81
+ ```
82
+
83
+ ```
84
+ Set the workspace default ruleset to https://github.example.com/org/standards/raw/main/ruleset.yaml
85
+ ```
86
+
87
+ ## Default Ruleset Configuration
88
+
89
+ All grading tools support an optional `rulesetPath` per-request. To avoid supplying it every time, configure a default:
90
+
91
+ - **Session** — in-memory, cleared on restart
92
+ - **Workspace** — saved to `.api-grade/config.json` in the project root
93
+ - **Global** — saved to `~/.api-grade/config.json`
94
+
95
+ Precedence: per-request → session → workspace → global → built-in.
96
+
97
+ Supported auth: GitHub PAT (`GITHUB_TOKEN` env var) and Microsoft Entra ID (device code flow).
98
+
99
+ ## Supported Spec Formats
100
+
101
+ - OpenAPI 2.x (Swagger)
102
+ - OpenAPI 3.x
103
+ - AsyncAPI 2.x
104
+ - AsyncAPI 3.x
105
+
106
+ ## Requirements
107
+
108
+ - Node.js ≥ 20.0.0
109
+
110
+ ## Related Packages
111
+
112
+ | Package | Purpose |
113
+ |---------|---------|
114
+ | [`@dawmatt/api-grade`](https://www.npmjs.com/package/@dawmatt/api-grade) | CLI tool — grade specs from the terminal |
115
+ | [`@dawmatt/api-grade-core`](https://www.npmjs.com/package/@dawmatt/api-grade-core) | Grading engine — embed in your own tools |
116
+ | [`@dawmatt/backstage-plugin-api-grade`](https://www.npmjs.com/package/@dawmatt/backstage-plugin-api-grade) | Backstage frontend card plugin |
117
+ | [`@dawmatt/backstage-plugin-api-grade-backend`](https://www.npmjs.com/package/@dawmatt/backstage-plugin-api-grade-backend) | Backstage backend grading plugin |
118
+
119
+ ## Documentation
120
+
121
+ - [Quick Start](https://github.com/DawMatt/api-grade/blob/main/docs/mcp/quick-start.md) — install and configure in minutes
122
+ - [Configuration Reference](https://github.com/DawMatt/api-grade/blob/main/docs/mcp/configuration.md) — default rulesets, auth, and scope precedence
123
+ - [Troubleshooting](https://github.com/DawMatt/api-grade/blob/main/docs/mcp/troubleshooting.md) — common issues and solutions
124
+ - [Full Documentation](https://github.com/DawMatt/api-grade)
125
+
126
+ ## License
127
+
128
+ MIT
@@ -0,0 +1,8 @@
1
+ export declare class EntraAuthRequired extends Error {
2
+ readonly userCode: string;
3
+ readonly verificationUri: string;
4
+ readonly expiresIn: number;
5
+ constructor(userCode: string, verificationUri: string, expiresIn: number);
6
+ }
7
+ export declare function acquireEntraToken(tenantId: string, clientId: string): Promise<string>;
8
+ //# sourceMappingURL=entra.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entra.d.ts","sourceRoot":"","sources":["../../src/auth/entra.ts"],"names":[],"mappings":"AAMA,qBAAa,iBAAkB,SAAQ,KAAK;aAExB,QAAQ,EAAE,MAAM;aAChB,eAAe,EAAE,MAAM;aACvB,SAAS,EAAE,MAAM;gBAFjB,QAAQ,EAAE,MAAM,EAChB,eAAe,EAAE,MAAM,EACvB,SAAS,EAAE,MAAM;CAKpC;AAED,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CA4C3F"}
@@ -0,0 +1,59 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ const ENTRA_CACHE_PATH = join(homedir(), '.api-grade', 'entra-token-cache.json');
5
+ export class EntraAuthRequired extends Error {
6
+ userCode;
7
+ verificationUri;
8
+ expiresIn;
9
+ constructor(userCode, verificationUri, expiresIn) {
10
+ super(`Entra ID authentication required. Visit ${verificationUri} and enter code ${userCode}.`);
11
+ this.userCode = userCode;
12
+ this.verificationUri = verificationUri;
13
+ this.expiresIn = expiresIn;
14
+ this.name = 'EntraAuthRequired';
15
+ }
16
+ }
17
+ export async function acquireEntraToken(tenantId, clientId) {
18
+ const { PublicClientApplication } = await import('@azure/msal-node');
19
+ const cachePlugin = {
20
+ async beforeCacheAccess(ctx) {
21
+ const data = await readFile(ENTRA_CACHE_PATH, 'utf-8').catch(() => '');
22
+ if (data)
23
+ ctx.tokenCache.deserialize(data);
24
+ },
25
+ async afterCacheAccess(ctx) {
26
+ if (ctx.cacheHasChanged) {
27
+ await mkdir(dirname(ENTRA_CACHE_PATH), { recursive: true });
28
+ await writeFile(ENTRA_CACHE_PATH, ctx.tokenCache.serialize(), 'utf-8');
29
+ }
30
+ },
31
+ };
32
+ const pca = new PublicClientApplication({
33
+ auth: { clientId, authority: `https://login.microsoftonline.com/${tenantId}` },
34
+ cache: { cachePlugin },
35
+ });
36
+ const scopes = [`api://${clientId}/.default`];
37
+ const accounts = await pca.getTokenCache().getAllAccounts();
38
+ if (accounts.length > 0) {
39
+ try {
40
+ const result = await pca.acquireTokenSilent({ scopes, account: accounts[0] });
41
+ if (result?.accessToken)
42
+ return result.accessToken;
43
+ }
44
+ catch {
45
+ // fall through to device-code flow
46
+ }
47
+ }
48
+ const result = await pca.acquireTokenByDeviceCode({
49
+ scopes,
50
+ deviceCodeCallback: (response) => {
51
+ throw new EntraAuthRequired(response.userCode, response.verificationUri, response.expiresIn);
52
+ },
53
+ });
54
+ if (!result?.accessToken) {
55
+ throw new Error('Entra ID token acquisition failed');
56
+ }
57
+ return result.accessToken;
58
+ }
59
+ //# sourceMappingURL=entra.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entra.js","sourceRoot":"","sources":["../../src/auth/entra.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare const INITIAL_FETCH_TIMEOUT_MS = 5000;
2
+ export declare const RETRY_FETCH_TIMEOUT_MS = 30000;
3
+ export declare class RulesetAuthError extends Error {
4
+ readonly reason: 'auth-failed' | 'network-unreachable';
5
+ readonly url: string;
6
+ constructor(reason: 'auth-failed' | 'network-unreachable', url: string);
7
+ }
8
+ export declare function fetchRulesetContent(url: string, token: string | undefined, timeoutMs: number): Promise<string>;
9
+ export declare function fetchRulesetWithGithubPat(url: string, token: string, timeoutMs: number): Promise<string>;
10
+ //# sourceMappingURL=github.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../../src/auth/github.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,wBAAwB,OAAQ,CAAC;AAC9C,eAAO,MAAM,sBAAsB,QAAS,CAAC;AAE7C,qBAAa,gBAAiB,SAAQ,KAAK;aAEvB,MAAM,EAAE,aAAa,GAAG,qBAAqB;aAC7C,GAAG,EAAE,MAAM;gBADX,MAAM,EAAE,aAAa,GAAG,qBAAqB,EAC7C,GAAG,EAAE,MAAM;CAK9B;AAED,wBAAsB,mBAAmB,CACvC,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,GAAG,SAAS,EACzB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAsBjB;AAED,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAEjB"}
@@ -0,0 +1,42 @@
1
+ export const INITIAL_FETCH_TIMEOUT_MS = 5_000;
2
+ export const RETRY_FETCH_TIMEOUT_MS = 30_000;
3
+ export class RulesetAuthError extends Error {
4
+ reason;
5
+ url;
6
+ constructor(reason, url) {
7
+ super(`Failed to fetch ruleset from ${url}: ${reason}`);
8
+ this.reason = reason;
9
+ this.url = url;
10
+ this.name = 'RulesetAuthError';
11
+ }
12
+ }
13
+ export async function fetchRulesetContent(url, token, timeoutMs) {
14
+ const controller = new AbortController();
15
+ const id = setTimeout(() => controller.abort(), timeoutMs);
16
+ const headers = token ? { Authorization: `Bearer ${token}` } : {};
17
+ try {
18
+ const res = await fetch(url, { headers, signal: controller.signal });
19
+ if (res.status === 401 || res.status === 403) {
20
+ throw new RulesetAuthError('auth-failed', url);
21
+ }
22
+ if (!res.ok) {
23
+ throw new RulesetAuthError('network-unreachable', url);
24
+ }
25
+ return res.text();
26
+ }
27
+ catch (e) {
28
+ if (e instanceof RulesetAuthError)
29
+ throw e;
30
+ if (e instanceof Error && e.name === 'AbortError') {
31
+ throw new RulesetAuthError('network-unreachable', url);
32
+ }
33
+ throw new RulesetAuthError('network-unreachable', url);
34
+ }
35
+ finally {
36
+ clearTimeout(id);
37
+ }
38
+ }
39
+ export async function fetchRulesetWithGithubPat(url, token, timeoutMs) {
40
+ return fetchRulesetContent(url, token, timeoutMs);
41
+ }
42
+ //# sourceMappingURL=github.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"github.js","sourceRoot":"","sources":["../../src/auth/github.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,CAAC;AAC9C,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAE7C,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAEvB;IACA;IAFlB,YACkB,MAA6C,EAC7C,GAAW;QAE3B,KAAK,CAAC,gCAAgC,GAAG,KAAK,MAAM,EAAE,CAAC,CAAC;QAHxC,WAAM,GAAN,MAAM,CAAuC;QAC7C,QAAG,GAAH,GAAG,CAAQ;QAG3B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,GAAW,EACX,KAAyB,EACzB,SAAiB;IAEjB,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,MAAM,EAAE,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,KAAK,EAAE,EAAE,SAAS,CAAC,CAAC;IAC3D,MAAM,OAAO,GAA2B,KAAK,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1F,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;QACrE,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC7C,MAAM,IAAI,gBAAgB,CAAC,aAAa,EAAE,GAAG,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,gBAAgB,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;QACzD,CAAC;QACD,OAAO,GAAG,CAAC,IAAI,EAAE,CAAC;IACpB,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,IAAI,CAAC,YAAY,gBAAgB;YAAE,MAAM,CAAC,CAAC;QAC3C,IAAI,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;YAClD,MAAM,IAAI,gBAAgB,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;QACzD,CAAC;QACD,MAAM,IAAI,gBAAgB,CAAC,qBAAqB,EAAE,GAAG,CAAC,CAAC;IACzD,CAAC;YAAS,CAAC;QACT,YAAY,CAAC,EAAE,CAAC,CAAC;IACnB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,yBAAyB,CAC7C,GAAW,EACX,KAAa,EACb,SAAiB;IAEjB,OAAO,mBAAmB,CAAC,GAAG,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { RulesetConfig, RulesetResolution, SessionState } from '../types.js';
2
+ export declare function resolveRuleset(perRequestPath: string | undefined | null, sessionState: SessionState, workspaceConfig: RulesetConfig | null, globalConfig: RulesetConfig | null): RulesetResolution;
3
+ //# sourceMappingURL=resolve-ruleset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-ruleset.d.ts","sourceRoot":"","sources":["../../src/config/resolve-ruleset.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAElF,wBAAgB,cAAc,CAC5B,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,EACzC,YAAY,EAAE,YAAY,EAC1B,eAAe,EAAE,aAAa,GAAG,IAAI,EACrC,YAAY,EAAE,aAAa,GAAG,IAAI,GACjC,iBAAiB,CAkCnB"}
@@ -0,0 +1,31 @@
1
+ export function resolveRuleset(perRequestPath, sessionState, workspaceConfig, globalConfig) {
2
+ if (perRequestPath != null && perRequestPath !== '') {
3
+ return { rulesetPath: perRequestPath, scope: 'per-request', auth: null };
4
+ }
5
+ if (sessionState.sessionRulesetOverride === 'builtin') {
6
+ return { rulesetPath: null, scope: 'built-in', auth: null };
7
+ }
8
+ if (sessionState.defaultRuleset?.rulesetPath != null) {
9
+ return {
10
+ rulesetPath: sessionState.defaultRuleset.rulesetPath,
11
+ scope: 'session',
12
+ auth: sessionState.defaultRuleset.auth,
13
+ };
14
+ }
15
+ if (workspaceConfig?.rulesetPath != null) {
16
+ return {
17
+ rulesetPath: workspaceConfig.rulesetPath,
18
+ scope: 'workspace',
19
+ auth: workspaceConfig.auth,
20
+ };
21
+ }
22
+ if (globalConfig?.rulesetPath != null) {
23
+ return {
24
+ rulesetPath: globalConfig.rulesetPath,
25
+ scope: 'global',
26
+ auth: globalConfig.auth,
27
+ };
28
+ }
29
+ return { rulesetPath: null, scope: 'built-in', auth: null };
30
+ }
31
+ //# sourceMappingURL=resolve-ruleset.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-ruleset.js","sourceRoot":"","sources":["../../src/config/resolve-ruleset.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,cAAc,CAC5B,cAAyC,EACzC,YAA0B,EAC1B,eAAqC,EACrC,YAAkC;IAElC,IAAI,cAAc,IAAI,IAAI,IAAI,cAAc,KAAK,EAAE,EAAE,CAAC;QACpD,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,KAAK,EAAE,aAAa,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC3E,CAAC;IAED,IAAI,YAAY,CAAC,sBAAsB,KAAK,SAAS,EAAE,CAAC;QACtD,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC9D,CAAC;IAED,IAAI,YAAY,CAAC,cAAc,EAAE,WAAW,IAAI,IAAI,EAAE,CAAC;QACrD,OAAO;YACL,WAAW,EAAE,YAAY,CAAC,cAAc,CAAC,WAAW;YACpD,KAAK,EAAE,SAAS;YAChB,IAAI,EAAE,YAAY,CAAC,cAAc,CAAC,IAAI;SACvC,CAAC;IACJ,CAAC;IAED,IAAI,eAAe,EAAE,WAAW,IAAI,IAAI,EAAE,CAAC;QACzC,OAAO;YACL,WAAW,EAAE,eAAe,CAAC,WAAW;YACxC,KAAK,EAAE,WAAW;YAClB,IAAI,EAAE,eAAe,CAAC,IAAI;SAC3B,CAAC;IACJ,CAAC;IAED,IAAI,YAAY,EAAE,WAAW,IAAI,IAAI,EAAE,CAAC;QACtC,OAAO;YACL,WAAW,EAAE,YAAY,CAAC,WAAW;YACrC,KAAK,EAAE,QAAQ;YACf,IAAI,EAAE,YAAY,CAAC,IAAI;SACxB,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC9D,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { RulesetConfig } from '../types.js';
2
+ export declare class ConfigWriteError extends Error {
3
+ readonly cause?: unknown | undefined;
4
+ readonly code = "CONFIG_WRITE_ERROR";
5
+ constructor(message: string, cause?: unknown | undefined);
6
+ }
7
+ export declare function getWorkspaceConfigPath(): string;
8
+ export declare function getGlobalConfigPath(): string;
9
+ export declare function loadWorkspaceConfig(): Promise<RulesetConfig | null>;
10
+ export declare function loadGlobalConfig(): Promise<RulesetConfig | null>;
11
+ export declare function saveWorkspaceConfig(config: RulesetConfig): Promise<void>;
12
+ export declare function saveGlobalConfig(config: RulesetConfig): Promise<void>;
13
+ //# sourceMappingURL=ruleset-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruleset-config.d.ts","sourceRoot":"","sources":["../../src/config/ruleset-config.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,qBAAa,gBAAiB,SAAQ,KAAK;IAEZ,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO;IADrD,QAAQ,CAAC,IAAI,wBAAwB;gBACzB,OAAO,EAAE,MAAM,EAAW,KAAK,CAAC,EAAE,OAAO,YAAA;CAItD;AAED,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAuBD,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAEzE;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAEtE;AAED,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9E;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAE3E"}
@@ -0,0 +1,49 @@
1
+ import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ export class ConfigWriteError extends Error {
5
+ cause;
6
+ code = 'CONFIG_WRITE_ERROR';
7
+ constructor(message, cause) {
8
+ super(message);
9
+ this.cause = cause;
10
+ this.name = 'ConfigWriteError';
11
+ }
12
+ }
13
+ export function getWorkspaceConfigPath() {
14
+ return join(process.cwd(), '.api-grade', 'config.json');
15
+ }
16
+ export function getGlobalConfigPath() {
17
+ return join(homedir(), '.api-grade', 'config.json');
18
+ }
19
+ async function loadConfig(filePath) {
20
+ try {
21
+ const data = await readFile(filePath, 'utf-8');
22
+ return JSON.parse(data);
23
+ }
24
+ catch {
25
+ return null;
26
+ }
27
+ }
28
+ async function saveConfig(filePath, config) {
29
+ try {
30
+ await mkdir(dirname(filePath), { recursive: true });
31
+ await writeFile(filePath, JSON.stringify(config, null, 2), 'utf-8');
32
+ }
33
+ catch (err) {
34
+ throw new ConfigWriteError(`Could not write config to ${filePath}: ${err instanceof Error ? err.message : String(err)}`, err);
35
+ }
36
+ }
37
+ export async function loadWorkspaceConfig() {
38
+ return loadConfig(getWorkspaceConfigPath());
39
+ }
40
+ export async function loadGlobalConfig() {
41
+ return loadConfig(getGlobalConfigPath());
42
+ }
43
+ export async function saveWorkspaceConfig(config) {
44
+ return saveConfig(getWorkspaceConfigPath(), config);
45
+ }
46
+ export async function saveGlobalConfig(config) {
47
+ return saveConfig(getGlobalConfigPath(), config);
48
+ }
49
+ //# sourceMappingURL=ruleset-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ruleset-config.js","sourceRoot":"","sources":["../../src/config/ruleset-config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC9D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAGlC,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAEH;IAD7B,IAAI,GAAG,oBAAoB,CAAC;IACrC,YAAY,OAAe,EAAW,KAAe;QACnD,KAAK,CAAC,OAAO,CAAC,CAAC;QADqB,UAAK,GAAL,KAAK,CAAU;QAEnD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;AAC1D,CAAC;AAED,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,aAAa,CAAC,CAAC;AACtD,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,QAAgB;IACxC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAkB,CAAC;IAC3C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,QAAgB,EAAE,MAAqB;IAC/D,IAAI,CAAC;QACH,MAAM,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,MAAM,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC;IACtE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,IAAI,gBAAgB,CACxB,6BAA6B,QAAQ,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,EAC5F,GAAG,CACJ,CAAC;IACJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,OAAO,UAAU,CAAC,sBAAsB,EAAE,CAAC,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,OAAO,UAAU,CAAC,mBAAmB,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAqB;IAC7D,OAAO,UAAU,CAAC,sBAAsB,EAAE,EAAE,MAAM,CAAC,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,MAAqB;IAC1D,OAAO,UAAU,CAAC,mBAAmB,EAAE,EAAE,MAAM,CAAC,CAAC;AACnD,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
2
+ import { createServer } from './server.js';
3
+ const server = createServer();
4
+ const transport = new StdioServerTransport();
5
+ await server.connect(transport);
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AACjF,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,GAAG,YAAY,EAAE,CAAC;AAC9B,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ export declare function createServer(): McpServer;
3
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAuBpE,wBAAgB,YAAY,IAAI,SAAS,CAUxC"}
package/dist/server.js ADDED
@@ -0,0 +1,33 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { createRequire } from 'node:module';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { resolve, dirname } from 'node:path';
5
+ import { registerGradeTool } from './tools/grade.js';
6
+ import { registerAssertGradeTool } from './tools/assert-grade.js';
7
+ import { registerGradeDetailedTool } from './tools/grade-detailed.js';
8
+ import { registerQuickFixesOnlyTool } from './tools/quick-fixes-only.js';
9
+ import { registerSetRulesetConfigTool } from './tools/set-ruleset-config.js';
10
+ import { registerGetRulesetConfigTool } from './tools/get-ruleset-config.js';
11
+ function getVersion() {
12
+ try {
13
+ const require = createRequire(import.meta.url);
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+ const pkg = require(resolve(__dirname, '../package.json'));
16
+ return pkg.version;
17
+ }
18
+ catch {
19
+ return '0.1.0';
20
+ }
21
+ }
22
+ export function createServer() {
23
+ const server = new McpServer({ name: 'api-grade', version: getVersion() });
24
+ const sessionState = { defaultRuleset: null, sessionRulesetOverride: null };
25
+ registerGradeTool(server, sessionState);
26
+ registerAssertGradeTool(server, sessionState);
27
+ registerGradeDetailedTool(server, sessionState);
28
+ registerQuickFixesOnlyTool(server, sessionState);
29
+ registerSetRulesetConfigTool(server, sessionState);
30
+ registerGetRulesetConfigTool(server, sessionState);
31
+ return server;
32
+ }
33
+ //# 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;AACpE,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAClE,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAC;AACzE,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAC7E,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAG7E,SAAS,UAAU;IACjB,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/C,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;QAC1D,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,iBAAiB,CAAC,CAAwB,CAAC;QAClF,OAAO,GAAG,CAAC,OAAO,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC;IACjB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,YAAY;IAC1B,MAAM,MAAM,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;IAC3E,MAAM,YAAY,GAAiB,EAAE,cAAc,EAAE,IAAI,EAAE,sBAAsB,EAAE,IAAI,EAAE,CAAC;IAC1F,iBAAiB,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACxC,uBAAuB,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC9C,yBAAyB,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAChD,0BAA0B,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,4BAA4B,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnD,4BAA4B,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACnD,OAAO,MAAM,CAAC;AAChB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { SessionState } from '../types.js';
3
+ export declare function registerAssertGradeTool(server: McpServer, sessionState: SessionState): void;
4
+ //# sourceMappingURL=assert-grade.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert-grade.d.ts","sourceRoot":"","sources":["../../src/tools/assert-grade.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAMzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,CAiJ3F"}
@@ -0,0 +1,132 @@
1
+ import { statSync, writeFileSync, unlinkSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { z } from 'zod';
5
+ import { GradeEngine, gradeToNumber, LETTER_GRADE_ORDER } from '@dawmatt/api-grade-core';
6
+ import { mcpError, buildAuthFailureResponse, ERROR_CODES } from '../utils/errors.js';
7
+ import { loadWorkspaceConfig, loadGlobalConfig } from '../config/ruleset-config.js';
8
+ import { resolveRuleset } from '../config/resolve-ruleset.js';
9
+ import { fetchRulesetContent, RulesetAuthError, INITIAL_FETCH_TIMEOUT_MS, RETRY_FETCH_TIMEOUT_MS } from '../auth/github.js';
10
+ import { EntraAuthRequired, acquireEntraToken } from '../auth/entra.js';
11
+ export function registerAssertGradeTool(server, sessionState) {
12
+ server.tool('assert-api-grade', 'Assert that an API specification meets a minimum grade threshold. Returns a structured pass/fail result. Use this in AI-assisted code review workflows or quality gates. Grade ordering: A (best) > B > C > D > F (worst).', {
13
+ specPath: z
14
+ .string()
15
+ .describe('Absolute or relative path to the OpenAPI or AsyncAPI specification file (YAML or JSON)'),
16
+ minimumGrade: z
17
+ .enum(['A', 'B', 'C', 'D', 'F'])
18
+ .describe('The minimum acceptable grade. The assertion passes if the actual grade is equal to or better than this value (A > B > C > D > F).'),
19
+ rulesetPath: z
20
+ .string()
21
+ .optional()
22
+ .describe('Optional path to a custom Spectral-compatible ruleset file'),
23
+ recoveryOption: z
24
+ .enum(['retry', 'use-builtin-once', 'use-builtin-session', 'cancel'])
25
+ .optional()
26
+ .describe('Recovery action when the configured default ruleset is inaccessible. Only supply in response to a RULESET_AUTH_FAILED response.'),
27
+ }, async ({ specPath, minimumGrade, rulesetPath, recoveryOption }) => {
28
+ if (!LETTER_GRADE_ORDER.includes(minimumGrade)) {
29
+ return mcpError(ERROR_CODES.INVALID_GRADE, `Invalid minimumGrade '${minimumGrade}'. Must be one of: A, B, C, D, F.`, { minimumGrade });
30
+ }
31
+ if (recoveryOption === 'cancel') {
32
+ return mcpError(ERROR_CODES.REQUEST_CANCELLED, 'Grading request cancelled by user.', { specPath });
33
+ }
34
+ if (recoveryOption === 'use-builtin-session') {
35
+ sessionState.sessionRulesetOverride = 'builtin';
36
+ }
37
+ const workspaceConfig = await loadWorkspaceConfig();
38
+ const globalConfig = await loadGlobalConfig();
39
+ const resolved = resolveRuleset(rulesetPath, sessionState, workspaceConfig, globalConfig);
40
+ let effectiveRulesetPath = resolved.rulesetPath ?? undefined;
41
+ let tempRulesetFile;
42
+ if (resolved.rulesetPath?.startsWith('http')) {
43
+ if (recoveryOption === 'use-builtin-once') {
44
+ effectiveRulesetPath = undefined;
45
+ }
46
+ else {
47
+ const timeoutMs = recoveryOption === 'retry' ? RETRY_FETCH_TIMEOUT_MS : INITIAL_FETCH_TIMEOUT_MS;
48
+ try {
49
+ let content;
50
+ if (resolved.auth?.type === 'github-pat') {
51
+ const token = resolved.auth.githubToken ?? process.env.GITHUB_TOKEN ?? '';
52
+ content = await fetchRulesetContent(resolved.rulesetPath, token || undefined, timeoutMs);
53
+ }
54
+ else if (resolved.auth?.type === 'entra-id' && resolved.auth.tenantId && resolved.auth.clientId) {
55
+ const token = await acquireEntraToken(resolved.auth.tenantId, resolved.auth.clientId);
56
+ content = await fetchRulesetContent(resolved.rulesetPath, token, timeoutMs);
57
+ }
58
+ else {
59
+ content = await fetchRulesetContent(resolved.rulesetPath, undefined, timeoutMs);
60
+ }
61
+ tempRulesetFile = join(tmpdir(), `api-grade-ruleset-${Date.now()}.yaml`);
62
+ writeFileSync(tempRulesetFile, content);
63
+ effectiveRulesetPath = tempRulesetFile;
64
+ }
65
+ catch (err) {
66
+ if (err instanceof EntraAuthRequired) {
67
+ return {
68
+ content: [{
69
+ type: 'text',
70
+ text: JSON.stringify({
71
+ error: ERROR_CODES.ENTRA_AUTH_REQUIRED,
72
+ deviceCodeUrl: err.verificationUri,
73
+ userCode: err.userCode,
74
+ expiresIn: err.expiresIn,
75
+ message: `Complete Entra ID sign-in: Visit ${err.verificationUri} and enter code ${err.userCode}`,
76
+ }),
77
+ }],
78
+ isError: true,
79
+ };
80
+ }
81
+ const reason = err instanceof RulesetAuthError ? err.reason : 'network-unreachable';
82
+ return buildAuthFailureResponse(reason, resolved.rulesetPath, resolved.scope, `Could not fetch ruleset from '${resolved.rulesetPath}' (${resolved.scope} default): ${reason.replace('-', ' ')}.`);
83
+ }
84
+ }
85
+ }
86
+ else if (effectiveRulesetPath) {
87
+ try {
88
+ statSync(effectiveRulesetPath);
89
+ }
90
+ catch {
91
+ return mcpError(ERROR_CODES.RULESET_NOT_FOUND, `The ruleset file '${effectiveRulesetPath}' does not exist. Check the path and try again.`, { rulesetPath: effectiveRulesetPath });
92
+ }
93
+ }
94
+ try {
95
+ statSync(specPath);
96
+ }
97
+ catch {
98
+ if (tempRulesetFile)
99
+ try {
100
+ unlinkSync(tempRulesetFile);
101
+ }
102
+ catch { /* ignore */ }
103
+ return mcpError(ERROR_CODES.SPEC_NOT_FOUND, `The specification file '${specPath}' does not exist. Check the path and try again.`, { specPath });
104
+ }
105
+ try {
106
+ const engine = new GradeEngine();
107
+ const result = await engine.grade({ specPath, rulesetPath: effectiveRulesetPath });
108
+ const actual = result.letterGrade;
109
+ const passed = gradeToNumber(actual) <= gradeToNumber(minimumGrade);
110
+ const response = {
111
+ passed,
112
+ actual,
113
+ minimum: minimumGrade,
114
+ specPath: result.specPath,
115
+ numericScore: result.numericScore,
116
+ };
117
+ return { content: [{ type: 'text', text: JSON.stringify(response) }] };
118
+ }
119
+ catch (err) {
120
+ const message = err instanceof Error ? err.message : String(err);
121
+ return mcpError(ERROR_CODES.GRADE_ENGINE_ERROR, `GradeEngine error: ${message}`, { specPath });
122
+ }
123
+ finally {
124
+ if (tempRulesetFile)
125
+ try {
126
+ unlinkSync(tempRulesetFile);
127
+ }
128
+ catch { /* ignore */ }
129
+ }
130
+ });
131
+ }
132
+ //# sourceMappingURL=assert-grade.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assert-grade.js","sourceRoot":"","sources":["../../src/tools/assert-grade.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import type { SessionState } from '../types.js';
3
+ export declare function registerConfigureRulesetTool(server: McpServer, sessionState: SessionState): void;
4
+ //# sourceMappingURL=configure-ruleset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"configure-ruleset.d.ts","sourceRoot":"","sources":["../../src/tools/configure-ruleset.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AASzE,OAAO,KAAK,EAAE,YAAY,EAA6B,MAAM,aAAa,CAAC;AAE3E,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,EAAE,YAAY,GAAG,IAAI,CAwHhG"}