spaps-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.
- package/CHANGELOG.md +7 -0
- package/README.md +76 -0
- package/dist/chunk-GPBTYWDD.js +496 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +9 -0
- package/dist/index.d.ts +103 -0
- package/dist/index.js +26 -0
- package/dist/resources/SPAPS_SURFACE_CONTRACT.md +125 -0
- package/dist/resources/glossary.md +36 -0
- package/dist/resources/llms.txt +15 -0
- package/dist/wizard/step-01-setup.md +149 -0
- package/dist/wizard/step-02-environment.md +291 -0
- package/dist/wizard/step-03-sdk-init.md +351 -0
- package/dist/wizard/step-04-email-auth.md +311 -0
- package/dist/wizard/step-05-wallet-auth.md +368 -0
- package/dist/wizard/step-06-magic-link.md +560 -0
- package/dist/wizard/step-07-payments.md +529 -0
- package/dist/wizard/step-08-whitelist.md +338 -0
- package/dist/wizard/step-09-admin.md +579 -0
- package/dist/wizard/step-10-errors.md +525 -0
- package/dist/wizard/step-11-ui-polish.md +640 -0
- package/dist/wizard/step-12-testing.md +588 -0
- package/dist/wizard/wizard.lock +67 -0
- package/package.json +66 -0
package/CHANGELOG.md
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# spaps-mcp
|
|
2
|
+
|
|
3
|
+
`spaps-mcp` exposes SPAPS documentation, endpoint contracts, auth discovery,
|
|
4
|
+
capability checks, and integration wizard steps to MCP-capable agents.
|
|
5
|
+
|
|
6
|
+
## Metadata
|
|
7
|
+
|
|
8
|
+
- `package_name`: `spaps-mcp`
|
|
9
|
+
- `latest_version`: `0.1.0`
|
|
10
|
+
- `minimum_runtime`: `Node.js >=18.0.0`
|
|
11
|
+
- `api_base_url`: `https://api.sweetpotato.dev`
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install spaps-mcp
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Claude Code
|
|
20
|
+
|
|
21
|
+
The Sweet Potato repo also includes a portable root `.mcp.example.json` for
|
|
22
|
+
local checkouts.
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
{
|
|
26
|
+
"mcpServers": {
|
|
27
|
+
"spaps": {
|
|
28
|
+
"command": "npx",
|
|
29
|
+
"args": ["spaps-mcp"],
|
|
30
|
+
"env": {
|
|
31
|
+
"SPAPS_API_URL": "http://localhost:3301",
|
|
32
|
+
"SPAPS_API_KEY": "spaps_pub_replace_me",
|
|
33
|
+
"SPAPS_ORIGIN": "http://localhost:3000"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Generic MCP
|
|
41
|
+
|
|
42
|
+
Run the stdio server directly:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
SPAPS_API_URL=https://api.sweetpotato.dev \
|
|
46
|
+
SPAPS_API_KEY=spaps_pub_replace_me \
|
|
47
|
+
SPAPS_ORIGIN=https://your-app.example \
|
|
48
|
+
npx spaps-mcp
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Secret-key tools are disabled by default. To enable graph contract and decision
|
|
52
|
+
explain tools, configure a secret SPAPS key and opt in explicitly:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
SPAPS_API_KEY=spaps_sec_replace_me \
|
|
56
|
+
SPAPS_MCP_ENABLE_SECRET_TOOLS=true \
|
|
57
|
+
npx spaps-mcp
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Tools
|
|
61
|
+
|
|
62
|
+
- `searchDocs`
|
|
63
|
+
- `getEndpoint`
|
|
64
|
+
- `getErrorCode`
|
|
65
|
+
- `getExample`
|
|
66
|
+
- `listAuthMethods`
|
|
67
|
+
- `capabilityCheck`
|
|
68
|
+
- `getCapabilityContract`
|
|
69
|
+
- `capabilityExplain`
|
|
70
|
+
- `listWizardSteps`
|
|
71
|
+
- `getWizardStep`
|
|
72
|
+
- `verifyIntegration`
|
|
73
|
+
|
|
74
|
+
Wizard steps are copied from the canonical
|
|
75
|
+
`agent-instructions/spaps-wizard-steps/` directory during package build. They are
|
|
76
|
+
not maintained by hand inside this package.
|
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// src/http.ts
|
|
2
|
+
var SpapsHttpError = class extends Error {
|
|
3
|
+
data;
|
|
4
|
+
constructor(data) {
|
|
5
|
+
super(data.message);
|
|
6
|
+
this.name = "SpapsHttpError";
|
|
7
|
+
this.data = data;
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
function resolveConfig(env = process.env) {
|
|
11
|
+
const apiUrl = (env.SPAPS_API_URL || "http://localhost:3301").replace(/\/+$/, "");
|
|
12
|
+
const enableSecretTools = ["1", "true", "yes", "on"].includes(
|
|
13
|
+
String(env.SPAPS_MCP_ENABLE_SECRET_TOOLS || "").toLowerCase()
|
|
14
|
+
);
|
|
15
|
+
return {
|
|
16
|
+
apiUrl,
|
|
17
|
+
apiKey: env.SPAPS_API_KEY || void 0,
|
|
18
|
+
bearerToken: env.SPAPS_AUTH_TOKEN || void 0,
|
|
19
|
+
origin: env.SPAPS_ORIGIN || void 0,
|
|
20
|
+
enableSecretTools,
|
|
21
|
+
wizardDir: env.SPAPS_WIZARD_STEPS_DIR || void 0
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function appendQuery(url, query) {
|
|
25
|
+
if (!query) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
for (const [key, value] of Object.entries(query)) {
|
|
29
|
+
if (value !== void 0) {
|
|
30
|
+
url.searchParams.set(key, String(value));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function unwrapEnvelope(payload, status, requestId) {
|
|
35
|
+
if (!payload || typeof payload !== "object") {
|
|
36
|
+
return payload;
|
|
37
|
+
}
|
|
38
|
+
const record = payload;
|
|
39
|
+
if (record.success === false) {
|
|
40
|
+
const error = typeof record.error === "object" && record.error ? record.error : {};
|
|
41
|
+
throw new SpapsHttpError({
|
|
42
|
+
status,
|
|
43
|
+
code: typeof error.code === "string" ? error.code : "SPAPS_ERROR",
|
|
44
|
+
message: typeof error.message === "string" ? error.message : "SPAPS request failed",
|
|
45
|
+
request_id: typeof record.request_id === "string" ? record.request_id : requestId,
|
|
46
|
+
details: error.details,
|
|
47
|
+
diagnostics: record.diagnostics,
|
|
48
|
+
remediations: record.remediations
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (record.success === true && Object.prototype.hasOwnProperty.call(record, "data")) {
|
|
52
|
+
return record.data;
|
|
53
|
+
}
|
|
54
|
+
return payload;
|
|
55
|
+
}
|
|
56
|
+
var SpapsHttpClient = class {
|
|
57
|
+
config;
|
|
58
|
+
fetchFn;
|
|
59
|
+
constructor(config, fetchFn = fetch) {
|
|
60
|
+
this.config = config;
|
|
61
|
+
this.fetchFn = fetchFn;
|
|
62
|
+
}
|
|
63
|
+
async get(path, options = {}) {
|
|
64
|
+
return this.request("GET", path, options);
|
|
65
|
+
}
|
|
66
|
+
async post(path, body, options = {}) {
|
|
67
|
+
return this.request("POST", path, { ...options, body });
|
|
68
|
+
}
|
|
69
|
+
async request(method, path, options = {}) {
|
|
70
|
+
const url = new URL(path, `${this.config.apiUrl}/`);
|
|
71
|
+
appendQuery(url, options.query);
|
|
72
|
+
const headers = {
|
|
73
|
+
accept: "application/json"
|
|
74
|
+
};
|
|
75
|
+
if (options.body !== void 0) {
|
|
76
|
+
headers["content-type"] = "application/json";
|
|
77
|
+
}
|
|
78
|
+
if (this.config.apiKey) {
|
|
79
|
+
headers["X-API-Key"] = this.config.apiKey;
|
|
80
|
+
}
|
|
81
|
+
if (this.config.bearerToken) {
|
|
82
|
+
headers.authorization = `Bearer ${this.config.bearerToken}`;
|
|
83
|
+
}
|
|
84
|
+
if (this.config.origin) {
|
|
85
|
+
headers.origin = this.config.origin;
|
|
86
|
+
}
|
|
87
|
+
const response = await this.fetchFn(url, {
|
|
88
|
+
method,
|
|
89
|
+
headers,
|
|
90
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
|
|
91
|
+
});
|
|
92
|
+
const requestId = response.headers.get("x-request-id") || void 0;
|
|
93
|
+
const text = await response.text();
|
|
94
|
+
const payload = text ? JSON.parse(text) : null;
|
|
95
|
+
if (!response.ok) {
|
|
96
|
+
const record = payload && typeof payload === "object" ? payload : {};
|
|
97
|
+
const error = typeof record.error === "object" && record.error ? record.error : {};
|
|
98
|
+
throw new SpapsHttpError({
|
|
99
|
+
status: response.status,
|
|
100
|
+
code: typeof error.code === "string" ? error.code : `HTTP_${response.status}`,
|
|
101
|
+
message: typeof error.message === "string" ? error.message : response.statusText,
|
|
102
|
+
request_id: typeof record.request_id === "string" ? record.request_id : requestId,
|
|
103
|
+
details: error.details,
|
|
104
|
+
diagnostics: record.diagnostics,
|
|
105
|
+
remediations: record.remediations
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return unwrapEnvelope(payload, response.status, requestId);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
function docsPath(method, path) {
|
|
112
|
+
const normalizedMethod = method.toUpperCase();
|
|
113
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
114
|
+
return `/api/docs/endpoints/${encodeURIComponent(normalizedMethod)}/${normalizedPath}`;
|
|
115
|
+
}
|
|
116
|
+
function docsPathWithCapturedLeadingSlash(method, path) {
|
|
117
|
+
const normalizedMethod = method.toUpperCase();
|
|
118
|
+
const normalizedPath = path.replace(/^\/+/, "");
|
|
119
|
+
return `/api/docs/endpoints/${encodeURIComponent(normalizedMethod)}//${normalizedPath}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/resources.ts
|
|
123
|
+
import { readFile } from "fs/promises";
|
|
124
|
+
import { dirname, resolve } from "path";
|
|
125
|
+
import { fileURLToPath } from "url";
|
|
126
|
+
function packagedResourcesDir() {
|
|
127
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "resources");
|
|
128
|
+
}
|
|
129
|
+
function resourceDefinition(name, filename, title, description, mimeType = "text/markdown") {
|
|
130
|
+
const uri = `spaps://resources/${filename}`;
|
|
131
|
+
return {
|
|
132
|
+
name,
|
|
133
|
+
uri,
|
|
134
|
+
title,
|
|
135
|
+
description,
|
|
136
|
+
mimeType,
|
|
137
|
+
async read() {
|
|
138
|
+
return {
|
|
139
|
+
contents: [{
|
|
140
|
+
uri,
|
|
141
|
+
mimeType,
|
|
142
|
+
text: await readFile(resolve(packagedResourcesDir(), filename), "utf8")
|
|
143
|
+
}]
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function buildResourceDefinitions() {
|
|
149
|
+
return [
|
|
150
|
+
resourceDefinition(
|
|
151
|
+
"llms",
|
|
152
|
+
"llms.txt",
|
|
153
|
+
"SPAPS llms.txt",
|
|
154
|
+
"Published SPAPS AI documentation index.",
|
|
155
|
+
"text/plain"
|
|
156
|
+
),
|
|
157
|
+
resourceDefinition(
|
|
158
|
+
"glossary",
|
|
159
|
+
"glossary.md",
|
|
160
|
+
"SPAPS Glossary",
|
|
161
|
+
"Shared SPAPS terminology and auth/payment vocabulary."
|
|
162
|
+
),
|
|
163
|
+
resourceDefinition(
|
|
164
|
+
"surfaceContract",
|
|
165
|
+
"SPAPS_SURFACE_CONTRACT.md",
|
|
166
|
+
"SPAPS Surface Contract",
|
|
167
|
+
"Contract boundaries for SPAPS CLI, SDK, docs, and server surfaces."
|
|
168
|
+
)
|
|
169
|
+
];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/wizard.ts
|
|
173
|
+
import { readFile as readFile2, readdir } from "fs/promises";
|
|
174
|
+
import { dirname as dirname2, resolve as resolve2 } from "path";
|
|
175
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
176
|
+
function packagedWizardDir() {
|
|
177
|
+
return resolve2(dirname2(fileURLToPath2(import.meta.url)), "wizard");
|
|
178
|
+
}
|
|
179
|
+
function resolveWizardDir(explicitDir) {
|
|
180
|
+
return explicitDir || packagedWizardDir();
|
|
181
|
+
}
|
|
182
|
+
function parseStepNumber(filename) {
|
|
183
|
+
const match = /^step-(\d+)-.*\.md$/.exec(filename);
|
|
184
|
+
return match ? Number.parseInt(match[1], 10) : null;
|
|
185
|
+
}
|
|
186
|
+
async function readLock(wizardDir) {
|
|
187
|
+
try {
|
|
188
|
+
const payload = JSON.parse(await readFile2(resolve2(wizardDir, "wizard.lock"), "utf8"));
|
|
189
|
+
return new Map((payload.steps || []).map((step) => [step.path, step.sha256]));
|
|
190
|
+
} catch {
|
|
191
|
+
return /* @__PURE__ */ new Map();
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async function stepFilenames(wizardDir) {
|
|
195
|
+
const entries = await readdir(wizardDir);
|
|
196
|
+
return entries.filter((entry) => entry.startsWith("step-") && entry.endsWith(".md")).sort();
|
|
197
|
+
}
|
|
198
|
+
async function titleFor(wizardDir, filename) {
|
|
199
|
+
const content = await readFile2(resolve2(wizardDir, filename), "utf8");
|
|
200
|
+
const firstLine = content.split(/\r?\n/, 1)[0] || filename;
|
|
201
|
+
return firstLine.replace(/^#\s*/, "").trim();
|
|
202
|
+
}
|
|
203
|
+
async function listWizardSteps(wizardDir) {
|
|
204
|
+
const lock = await readLock(wizardDir);
|
|
205
|
+
const files = await stepFilenames(wizardDir);
|
|
206
|
+
const steps = await Promise.all(files.map(async (filename) => {
|
|
207
|
+
const step = parseStepNumber(filename);
|
|
208
|
+
if (step === null) {
|
|
209
|
+
throw new Error(`Invalid wizard step filename: ${filename}`);
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
step,
|
|
213
|
+
path: filename,
|
|
214
|
+
title: await titleFor(wizardDir, filename),
|
|
215
|
+
sha256: lock.get(filename)
|
|
216
|
+
};
|
|
217
|
+
}));
|
|
218
|
+
return { count: steps.length, steps };
|
|
219
|
+
}
|
|
220
|
+
async function getWizardStep(wizardDir, stepNumber) {
|
|
221
|
+
const listed = await listWizardSteps(wizardDir);
|
|
222
|
+
const summary = listed.steps.find((step) => step.step === stepNumber);
|
|
223
|
+
if (!summary) {
|
|
224
|
+
throw new Error(`Wizard step ${stepNumber} not found`);
|
|
225
|
+
}
|
|
226
|
+
const content = await readFile2(resolve2(wizardDir, summary.path), "utf8");
|
|
227
|
+
return { ...summary, content };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/tools.ts
|
|
231
|
+
import { z } from "zod";
|
|
232
|
+
function jsonResult(payload) {
|
|
233
|
+
return {
|
|
234
|
+
content: [{ type: "text", text: JSON.stringify(payload, null, 2) }]
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function errorResult(error) {
|
|
238
|
+
if (error instanceof SpapsHttpError) {
|
|
239
|
+
return jsonResult({
|
|
240
|
+
ok: false,
|
|
241
|
+
error: error.data
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
isError: true,
|
|
246
|
+
content: [{
|
|
247
|
+
type: "text",
|
|
248
|
+
text: JSON.stringify({
|
|
249
|
+
ok: false,
|
|
250
|
+
error: {
|
|
251
|
+
code: "SPAPS_MCP_ERROR",
|
|
252
|
+
message: error instanceof Error ? error.message : String(error)
|
|
253
|
+
}
|
|
254
|
+
}, null, 2)
|
|
255
|
+
}]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
function guarded(handler) {
|
|
259
|
+
return async (input) => {
|
|
260
|
+
try {
|
|
261
|
+
return await handler(input);
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const result = errorResult(error);
|
|
264
|
+
result.isError = true;
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
function requireSecretTools(config) {
|
|
270
|
+
if (config.enableSecretTools) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
isError: true,
|
|
275
|
+
content: [{
|
|
276
|
+
type: "text",
|
|
277
|
+
text: JSON.stringify({
|
|
278
|
+
ok: false,
|
|
279
|
+
error: {
|
|
280
|
+
code: "SPAPS_MCP_SECRET_TOOL_DISABLED",
|
|
281
|
+
message: "Set SPAPS_MCP_ENABLE_SECRET_TOOLS=true and configure a secret SPAPS_API_KEY before using this tool."
|
|
282
|
+
}
|
|
283
|
+
}, null, 2)
|
|
284
|
+
}]
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
function buildToolDefinitions({ config, fetchFn, client = new SpapsHttpClient(config, fetchFn) }) {
|
|
288
|
+
const wizardDir = resolveWizardDir(config.wizardDir);
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
name: "searchDocs",
|
|
292
|
+
title: "Search SPAPS Docs",
|
|
293
|
+
description: "Search SPAPS documentation through the authenticated docs search API.",
|
|
294
|
+
inputSchema: {
|
|
295
|
+
query: z.string().min(3),
|
|
296
|
+
context: z.string().optional(),
|
|
297
|
+
limit: z.number().int().min(1).max(20).optional(),
|
|
298
|
+
includeExamples: z.boolean().optional(),
|
|
299
|
+
language: z.string().optional()
|
|
300
|
+
},
|
|
301
|
+
handler: guarded(async (input) => jsonResult(await client.post("/api/docs/search", input)))
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "getEndpoint",
|
|
305
|
+
title: "Get Endpoint Metadata",
|
|
306
|
+
description: "Fetch endpoint docs and manifest metadata for one SPAPS route.",
|
|
307
|
+
inputSchema: {
|
|
308
|
+
method: z.string().min(1),
|
|
309
|
+
path: z.string().min(1)
|
|
310
|
+
},
|
|
311
|
+
handler: guarded(async (input) => {
|
|
312
|
+
const method = String(input.method);
|
|
313
|
+
const path = String(input.path);
|
|
314
|
+
try {
|
|
315
|
+
return jsonResult(await client.get(docsPath(method, path)));
|
|
316
|
+
} catch (error) {
|
|
317
|
+
if (error instanceof SpapsHttpError && error.data.status === 404) {
|
|
318
|
+
return jsonResult(await client.get(docsPathWithCapturedLeadingSlash(method, path)));
|
|
319
|
+
}
|
|
320
|
+
throw error;
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "getErrorCode",
|
|
326
|
+
title: "Get Error Code",
|
|
327
|
+
description: "Fetch SPAPS error-code documentation.",
|
|
328
|
+
inputSchema: {
|
|
329
|
+
code: z.string().min(1),
|
|
330
|
+
includeExamples: z.boolean().optional()
|
|
331
|
+
},
|
|
332
|
+
handler: guarded(async (input) => jsonResult(await client.get(
|
|
333
|
+
`/api/docs/errors/${encodeURIComponent(String(input.code))}`,
|
|
334
|
+
{ query: { includeExamples: input.includeExamples } }
|
|
335
|
+
)))
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
name: "getExample",
|
|
339
|
+
title: "Get Example",
|
|
340
|
+
description: "Fetch a named SPAPS integration example.",
|
|
341
|
+
inputSchema: {
|
|
342
|
+
feature: z.string().min(1),
|
|
343
|
+
language: z.string().optional()
|
|
344
|
+
},
|
|
345
|
+
handler: guarded(async (input) => jsonResult(await client.get(
|
|
346
|
+
`/api/docs/examples/${encodeURIComponent(String(input.feature))}`,
|
|
347
|
+
{ query: { language: input.language } }
|
|
348
|
+
)))
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "listAuthMethods",
|
|
352
|
+
title: "List Auth Methods",
|
|
353
|
+
description: "Return the public auth-method matrix for the configured SPAPS application key.",
|
|
354
|
+
inputSchema: {},
|
|
355
|
+
handler: guarded(async () => jsonResult(await client.get("/api/auth/methods")))
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "capabilityCheck",
|
|
359
|
+
title: "Capability Check",
|
|
360
|
+
description: "Run a SPAPS access decision. Publishable callers must provide a self-scoped bearer token when required by the API.",
|
|
361
|
+
inputSchema: {
|
|
362
|
+
actor: z.record(z.string(), z.unknown()),
|
|
363
|
+
action: z.string().min(1),
|
|
364
|
+
resource: z.record(z.string(), z.unknown()),
|
|
365
|
+
controls: z.record(z.string(), z.unknown()).optional(),
|
|
366
|
+
context: z.record(z.string(), z.unknown()).optional()
|
|
367
|
+
},
|
|
368
|
+
handler: guarded(async (input) => jsonResult(await client.post("/api/access/decide", input)))
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
name: "getCapabilityContract",
|
|
372
|
+
title: "Get Capability Contract",
|
|
373
|
+
description: "Fetch the machine-readable capability graph contract. Requires explicit secret-tool opt-in.",
|
|
374
|
+
inputSchema: {},
|
|
375
|
+
handler: guarded(async () => {
|
|
376
|
+
const disabled = requireSecretTools(config);
|
|
377
|
+
return disabled || jsonResult(await client.get("/api/contract"));
|
|
378
|
+
})
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
name: "capabilityExplain",
|
|
382
|
+
title: "Explain Capability Decision",
|
|
383
|
+
description: "Explain a prior capability decision trace by ID. Requires explicit secret-tool opt-in.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
decision_id: z.string().uuid()
|
|
386
|
+
},
|
|
387
|
+
handler: guarded(async (input) => {
|
|
388
|
+
const disabled = requireSecretTools(config);
|
|
389
|
+
return disabled || jsonResult(await client.get(`/api/graph/explain/${encodeURIComponent(String(input.decision_id))}`));
|
|
390
|
+
})
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "listWizardSteps",
|
|
394
|
+
title: "List Wizard Steps",
|
|
395
|
+
description: "List bundled SPAPS Integration Wizard steps from the canonical build artifact.",
|
|
396
|
+
inputSchema: {},
|
|
397
|
+
handler: guarded(async () => jsonResult(await listWizardSteps(wizardDir)))
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: "getWizardStep",
|
|
401
|
+
title: "Get Wizard Step",
|
|
402
|
+
description: "Return one SPAPS Integration Wizard step by number.",
|
|
403
|
+
inputSchema: {
|
|
404
|
+
step: z.number().int().min(1).max(12)
|
|
405
|
+
},
|
|
406
|
+
handler: guarded(async (input) => jsonResult(await getWizardStep(wizardDir, Number(input.step))))
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: "verifyIntegration",
|
|
410
|
+
title: "Verify SPAPS Integration",
|
|
411
|
+
description: "Run non-mutating health checks against the configured or supplied SPAPS base URL.",
|
|
412
|
+
inputSchema: {
|
|
413
|
+
target_base_url: z.string().url().optional(),
|
|
414
|
+
include_auth_methods: z.boolean().optional()
|
|
415
|
+
},
|
|
416
|
+
handler: guarded(async (input) => {
|
|
417
|
+
const targetConfig = input.target_base_url ? { ...config, apiUrl: String(input.target_base_url).replace(/\/+$/, "") } : config;
|
|
418
|
+
const targetClient = input.target_base_url ? new SpapsHttpClient(targetConfig, fetchFn) : client;
|
|
419
|
+
const results = [
|
|
420
|
+
{ test: "health", success: false, details: null },
|
|
421
|
+
{ test: "ready", success: false, details: null },
|
|
422
|
+
{ test: "local_mode_contract", success: false, details: null }
|
|
423
|
+
];
|
|
424
|
+
results[0].details = await targetClient.get("/health");
|
|
425
|
+
results[0].success = true;
|
|
426
|
+
results[1].details = await targetClient.get("/health/ready");
|
|
427
|
+
results[1].success = true;
|
|
428
|
+
results[2].details = await targetClient.get("/health/local-mode");
|
|
429
|
+
results[2].success = true;
|
|
430
|
+
if (input.include_auth_methods) {
|
|
431
|
+
results.push({
|
|
432
|
+
test: "auth_methods",
|
|
433
|
+
success: true,
|
|
434
|
+
details: await targetClient.get("/api/auth/methods")
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
return jsonResult({
|
|
438
|
+
success: true,
|
|
439
|
+
summary: `${results.filter((result) => result.success).length}/${results.length} tests passed`,
|
|
440
|
+
target_base_url: targetConfig.apiUrl,
|
|
441
|
+
results,
|
|
442
|
+
next_steps: [`See docs at ${targetConfig.apiUrl}/docs`]
|
|
443
|
+
});
|
|
444
|
+
})
|
|
445
|
+
}
|
|
446
|
+
];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// src/server.ts
|
|
450
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
451
|
+
function createSpapsMcpServer(options = {}) {
|
|
452
|
+
const config = options.config || resolveConfig();
|
|
453
|
+
const client = new SpapsHttpClient(config, options.fetchFn);
|
|
454
|
+
const server = new McpServer({
|
|
455
|
+
name: "spaps-mcp",
|
|
456
|
+
version: "0.1.0"
|
|
457
|
+
});
|
|
458
|
+
for (const tool of buildToolDefinitions({ config, client })) {
|
|
459
|
+
server.registerTool(
|
|
460
|
+
tool.name,
|
|
461
|
+
{
|
|
462
|
+
title: tool.title,
|
|
463
|
+
description: tool.description,
|
|
464
|
+
inputSchema: tool.inputSchema
|
|
465
|
+
},
|
|
466
|
+
tool.handler
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
for (const resource of buildResourceDefinitions()) {
|
|
470
|
+
server.registerResource(
|
|
471
|
+
resource.name,
|
|
472
|
+
resource.uri,
|
|
473
|
+
{
|
|
474
|
+
title: resource.title,
|
|
475
|
+
description: resource.description,
|
|
476
|
+
mimeType: resource.mimeType
|
|
477
|
+
},
|
|
478
|
+
resource.read
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return server;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export {
|
|
485
|
+
SpapsHttpError,
|
|
486
|
+
resolveConfig,
|
|
487
|
+
SpapsHttpClient,
|
|
488
|
+
docsPath,
|
|
489
|
+
docsPathWithCapturedLeadingSlash,
|
|
490
|
+
buildResourceDefinitions,
|
|
491
|
+
resolveWizardDir,
|
|
492
|
+
listWizardSteps,
|
|
493
|
+
getWizardStep,
|
|
494
|
+
buildToolDefinitions,
|
|
495
|
+
createSpapsMcpServer
|
|
496
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createSpapsMcpServer
|
|
4
|
+
} from "./chunk-GPBTYWDD.js";
|
|
5
|
+
|
|
6
|
+
// src/cli.ts
|
|
7
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
8
|
+
var server = createSpapsMcpServer();
|
|
9
|
+
await server.connect(new StdioServerTransport());
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
|
|
4
|
+
interface SpapsMcpConfig {
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
bearerToken?: string;
|
|
8
|
+
origin?: string;
|
|
9
|
+
enableSecretTools: boolean;
|
|
10
|
+
wizardDir?: string;
|
|
11
|
+
}
|
|
12
|
+
interface RequestOptions {
|
|
13
|
+
body?: unknown;
|
|
14
|
+
query?: Record<string, string | number | boolean | undefined>;
|
|
15
|
+
}
|
|
16
|
+
type FetchLike = typeof fetch;
|
|
17
|
+
interface SpapsHttpErrorData {
|
|
18
|
+
status: number;
|
|
19
|
+
code: string;
|
|
20
|
+
message: string;
|
|
21
|
+
request_id?: string;
|
|
22
|
+
details?: unknown;
|
|
23
|
+
diagnostics?: unknown;
|
|
24
|
+
remediations?: unknown;
|
|
25
|
+
}
|
|
26
|
+
declare class SpapsHttpError extends Error {
|
|
27
|
+
data: SpapsHttpErrorData;
|
|
28
|
+
constructor(data: SpapsHttpErrorData);
|
|
29
|
+
}
|
|
30
|
+
declare function resolveConfig(env?: NodeJS.ProcessEnv): SpapsMcpConfig;
|
|
31
|
+
declare class SpapsHttpClient {
|
|
32
|
+
private readonly config;
|
|
33
|
+
private readonly fetchFn;
|
|
34
|
+
constructor(config: SpapsMcpConfig, fetchFn?: FetchLike);
|
|
35
|
+
get(path: string, options?: RequestOptions): Promise<unknown>;
|
|
36
|
+
post(path: string, body?: unknown, options?: RequestOptions): Promise<unknown>;
|
|
37
|
+
request(method: string, path: string, options?: RequestOptions): Promise<unknown>;
|
|
38
|
+
}
|
|
39
|
+
declare function docsPath(method: string, path: string): string;
|
|
40
|
+
declare function docsPathWithCapturedLeadingSlash(method: string, path: string): string;
|
|
41
|
+
|
|
42
|
+
interface CreateServerOptions {
|
|
43
|
+
config?: SpapsMcpConfig;
|
|
44
|
+
fetchFn?: FetchLike;
|
|
45
|
+
}
|
|
46
|
+
declare function createSpapsMcpServer(options?: CreateServerOptions): McpServer;
|
|
47
|
+
|
|
48
|
+
interface ToolResult {
|
|
49
|
+
[key: string]: unknown;
|
|
50
|
+
content: Array<{
|
|
51
|
+
type: 'text';
|
|
52
|
+
text: string;
|
|
53
|
+
}>;
|
|
54
|
+
isError?: boolean;
|
|
55
|
+
}
|
|
56
|
+
type ToolHandler = (input: Record<string, unknown>) => Promise<ToolResult>;
|
|
57
|
+
interface ToolDefinition {
|
|
58
|
+
name: string;
|
|
59
|
+
title: string;
|
|
60
|
+
description: string;
|
|
61
|
+
inputSchema: Record<string, z.ZodTypeAny>;
|
|
62
|
+
handler: ToolHandler;
|
|
63
|
+
}
|
|
64
|
+
interface ToolBuildOptions {
|
|
65
|
+
config: SpapsMcpConfig;
|
|
66
|
+
client?: SpapsHttpClient;
|
|
67
|
+
fetchFn?: FetchLike;
|
|
68
|
+
}
|
|
69
|
+
declare function buildToolDefinitions({ config, fetchFn, client }: ToolBuildOptions): ToolDefinition[];
|
|
70
|
+
|
|
71
|
+
interface ResourceDefinition {
|
|
72
|
+
name: string;
|
|
73
|
+
uri: string;
|
|
74
|
+
title: string;
|
|
75
|
+
description: string;
|
|
76
|
+
mimeType: string;
|
|
77
|
+
read(): Promise<{
|
|
78
|
+
contents: Array<{
|
|
79
|
+
uri: string;
|
|
80
|
+
mimeType: string;
|
|
81
|
+
text: string;
|
|
82
|
+
}>;
|
|
83
|
+
}>;
|
|
84
|
+
}
|
|
85
|
+
declare function buildResourceDefinitions(): ResourceDefinition[];
|
|
86
|
+
|
|
87
|
+
interface WizardStepSummary {
|
|
88
|
+
step: number;
|
|
89
|
+
path: string;
|
|
90
|
+
title: string;
|
|
91
|
+
sha256?: string;
|
|
92
|
+
}
|
|
93
|
+
interface WizardStep extends WizardStepSummary {
|
|
94
|
+
content: string;
|
|
95
|
+
}
|
|
96
|
+
declare function resolveWizardDir(explicitDir?: string): string;
|
|
97
|
+
declare function listWizardSteps(wizardDir: string): Promise<{
|
|
98
|
+
count: number;
|
|
99
|
+
steps: WizardStepSummary[];
|
|
100
|
+
}>;
|
|
101
|
+
declare function getWizardStep(wizardDir: string, stepNumber: number): Promise<WizardStep>;
|
|
102
|
+
|
|
103
|
+
export { type ResourceDefinition, SpapsHttpClient, SpapsHttpError, type SpapsHttpErrorData, type SpapsMcpConfig, type ToolDefinition, type ToolResult, type WizardStep, type WizardStepSummary, buildResourceDefinitions, buildToolDefinitions, createSpapsMcpServer, docsPath, docsPathWithCapturedLeadingSlash, getWizardStep, listWizardSteps, resolveConfig, resolveWizardDir };
|