spaps-mcp 0.1.1 → 0.1.2
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 +4 -0
- package/README.md +1 -1
- package/dist/{chunk-GPBTYWDD.js → chunk-WWRSSH7A.js} +81 -25
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ capability checks, and integration wizard steps to MCP-capable agents.
|
|
|
6
6
|
## Metadata
|
|
7
7
|
|
|
8
8
|
- `package_name`: `spaps-mcp`
|
|
9
|
-
- `latest_version`: `0.1.
|
|
9
|
+
- `latest_version`: `0.1.1`
|
|
10
10
|
- `minimum_runtime`: `Node.js >=18.0.0`
|
|
11
11
|
- `api_base_url`: `https://api.sweetpotato.dev`
|
|
12
12
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/http.ts
|
|
2
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 15e3;
|
|
2
3
|
var SpapsHttpError = class extends Error {
|
|
3
4
|
data;
|
|
4
5
|
constructor(data) {
|
|
@@ -7,6 +8,16 @@ var SpapsHttpError = class extends Error {
|
|
|
7
8
|
this.data = data;
|
|
8
9
|
}
|
|
9
10
|
};
|
|
11
|
+
function parsePositiveIntEnv(value) {
|
|
12
|
+
if (value === void 0 || value.trim() === "") {
|
|
13
|
+
return void 0;
|
|
14
|
+
}
|
|
15
|
+
const parsed = Number(value);
|
|
16
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
17
|
+
return void 0;
|
|
18
|
+
}
|
|
19
|
+
return Math.floor(parsed);
|
|
20
|
+
}
|
|
10
21
|
function resolveConfig(env = process.env) {
|
|
11
22
|
const apiUrl = (env.SPAPS_API_URL || "http://localhost:3301").replace(/\/+$/, "");
|
|
12
23
|
const enableSecretTools = ["1", "true", "yes", "on"].includes(
|
|
@@ -18,7 +29,8 @@ function resolveConfig(env = process.env) {
|
|
|
18
29
|
bearerToken: env.SPAPS_AUTH_TOKEN || void 0,
|
|
19
30
|
origin: env.SPAPS_ORIGIN || void 0,
|
|
20
31
|
enableSecretTools,
|
|
21
|
-
wizardDir: env.SPAPS_WIZARD_STEPS_DIR || void 0
|
|
32
|
+
wizardDir: env.SPAPS_WIZARD_STEPS_DIR || void 0,
|
|
33
|
+
requestTimeoutMs: parsePositiveIntEnv(env.SPAPS_MCP_REQUEST_TIMEOUT_MS) ?? DEFAULT_REQUEST_TIMEOUT_MS
|
|
22
34
|
};
|
|
23
35
|
}
|
|
24
36
|
function appendQuery(url, query) {
|
|
@@ -53,12 +65,22 @@ function unwrapEnvelope(payload, status, requestId) {
|
|
|
53
65
|
}
|
|
54
66
|
return payload;
|
|
55
67
|
}
|
|
68
|
+
function isTimeoutAbort(err) {
|
|
69
|
+
if (typeof err !== "object" || err === null || !("name" in err)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const name = err.name;
|
|
73
|
+
return name === "TimeoutError" || name === "AbortError";
|
|
74
|
+
}
|
|
56
75
|
var SpapsHttpClient = class {
|
|
57
76
|
config;
|
|
58
77
|
fetchFn;
|
|
78
|
+
requestTimeoutMs;
|
|
59
79
|
constructor(config, fetchFn = fetch) {
|
|
60
80
|
this.config = config;
|
|
61
81
|
this.fetchFn = fetchFn;
|
|
82
|
+
const configured = config.requestTimeoutMs;
|
|
83
|
+
this.requestTimeoutMs = typeof configured === "number" && configured > 0 ? configured : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
62
84
|
}
|
|
63
85
|
async get(path, options = {}) {
|
|
64
86
|
return this.request("GET", path, options);
|
|
@@ -84,14 +106,34 @@ var SpapsHttpClient = class {
|
|
|
84
106
|
if (this.config.origin) {
|
|
85
107
|
headers.origin = this.config.origin;
|
|
86
108
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
let response;
|
|
110
|
+
try {
|
|
111
|
+
response = await this.fetchFn(url, {
|
|
112
|
+
method,
|
|
113
|
+
headers,
|
|
114
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
115
|
+
signal: AbortSignal.timeout(this.requestTimeoutMs)
|
|
116
|
+
});
|
|
117
|
+
} catch (err) {
|
|
118
|
+
if (isTimeoutAbort(err)) {
|
|
119
|
+
throw new SpapsHttpError({
|
|
120
|
+
status: 504,
|
|
121
|
+
code: "SPAPS_REQUEST_TIMEOUT",
|
|
122
|
+
message: `SPAPS request timed out after ${this.requestTimeoutMs}ms`
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
92
127
|
const requestId = response.headers.get("x-request-id") || void 0;
|
|
93
128
|
const text = await response.text();
|
|
94
|
-
|
|
129
|
+
let payload = null;
|
|
130
|
+
if (text) {
|
|
131
|
+
try {
|
|
132
|
+
payload = JSON.parse(text);
|
|
133
|
+
} catch {
|
|
134
|
+
payload = null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
95
137
|
if (!response.ok) {
|
|
96
138
|
const record = payload && typeof payload === "object" ? payload : {};
|
|
97
139
|
const error = typeof record.error === "object" && record.error ? record.error : {};
|
|
@@ -414,29 +456,43 @@ function buildToolDefinitions({ config, fetchFn, client = new SpapsHttpClient(co
|
|
|
414
456
|
include_auth_methods: z.boolean().optional()
|
|
415
457
|
},
|
|
416
458
|
handler: guarded(async (input) => {
|
|
417
|
-
const targetConfig = input.target_base_url ? {
|
|
459
|
+
const targetConfig = input.target_base_url ? {
|
|
460
|
+
...config,
|
|
461
|
+
apiUrl: String(input.target_base_url).replace(/\/+$/, ""),
|
|
462
|
+
// Health checks are unauthenticated. Never forward the operator's
|
|
463
|
+
// configured credentials to an agent-supplied (and therefore
|
|
464
|
+
// potentially external/attacker-controlled) host — doing so would
|
|
465
|
+
// exfiltrate the secret API key + bearer JWT via SSRF.
|
|
466
|
+
apiKey: void 0,
|
|
467
|
+
bearerToken: void 0,
|
|
468
|
+
origin: void 0
|
|
469
|
+
} : config;
|
|
418
470
|
const targetClient = input.target_base_url ? new SpapsHttpClient(targetConfig, fetchFn) : client;
|
|
419
|
-
const
|
|
420
|
-
{ test: "health",
|
|
421
|
-
{ test: "ready",
|
|
422
|
-
{ test: "local_mode_contract",
|
|
471
|
+
const checks = [
|
|
472
|
+
{ test: "health", path: "/health" },
|
|
473
|
+
{ test: "ready", path: "/health/ready" },
|
|
474
|
+
{ test: "local_mode_contract", path: "/health/local-mode" }
|
|
423
475
|
];
|
|
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
476
|
if (input.include_auth_methods) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
477
|
+
checks.push({ test: "auth_methods", path: "/api/auth/methods" });
|
|
478
|
+
}
|
|
479
|
+
const results = [];
|
|
480
|
+
for (const check of checks) {
|
|
481
|
+
try {
|
|
482
|
+
const details = await targetClient.get(check.path);
|
|
483
|
+
results.push({ test: check.test, success: true, details });
|
|
484
|
+
} catch (error) {
|
|
485
|
+
results.push({
|
|
486
|
+
test: check.test,
|
|
487
|
+
success: false,
|
|
488
|
+
details: error instanceof SpapsHttpError ? error.data : { error: error instanceof Error ? error.message : String(error) }
|
|
489
|
+
});
|
|
490
|
+
}
|
|
436
491
|
}
|
|
492
|
+
const passed = results.filter((result) => result.success).length;
|
|
437
493
|
return jsonResult({
|
|
438
|
-
success:
|
|
439
|
-
summary: `${
|
|
494
|
+
success: passed === results.length,
|
|
495
|
+
summary: `${passed}/${results.length} tests passed`,
|
|
440
496
|
target_base_url: targetConfig.apiUrl,
|
|
441
497
|
results,
|
|
442
498
|
next_steps: [`See docs at ${targetConfig.apiUrl}/docs`]
|
package/dist/cli.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ interface SpapsMcpConfig {
|
|
|
8
8
|
origin?: string;
|
|
9
9
|
enableSecretTools: boolean;
|
|
10
10
|
wizardDir?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Per-request timeout in milliseconds. A hung upstream would otherwise wedge
|
|
13
|
+
* an MCP tool forever; the outbound fetch is aborted after this many ms and
|
|
14
|
+
* surfaced as a {@link SpapsHttpError} timeout. Configurable via
|
|
15
|
+
* `SPAPS_MCP_REQUEST_TIMEOUT_MS`; defaults to {@link DEFAULT_REQUEST_TIMEOUT_MS}.
|
|
16
|
+
*/
|
|
17
|
+
requestTimeoutMs?: number;
|
|
11
18
|
}
|
|
12
19
|
interface RequestOptions {
|
|
13
20
|
body?: unknown;
|
|
@@ -31,6 +38,7 @@ declare function resolveConfig(env?: NodeJS.ProcessEnv): SpapsMcpConfig;
|
|
|
31
38
|
declare class SpapsHttpClient {
|
|
32
39
|
private readonly config;
|
|
33
40
|
private readonly fetchFn;
|
|
41
|
+
private readonly requestTimeoutMs;
|
|
34
42
|
constructor(config: SpapsMcpConfig, fetchFn?: FetchLike);
|
|
35
43
|
get(path: string, options?: RequestOptions): Promise<unknown>;
|
|
36
44
|
post(path: string, body?: unknown, options?: RequestOptions): Promise<unknown>;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED