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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 - 2026-06-20
4
+
5
+ - Maintenance: align the MCP package version after release automation.
6
+
3
7
  ## 0.1.0 - 2026-06-15
4
8
 
5
9
  - Initial SPAPS MCP stdio server with docs, auth discovery, capability, wizard,
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.0`
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
- const response = await this.fetchFn(url, {
88
- method,
89
- headers,
90
- body: options.body === void 0 ? void 0 : JSON.stringify(options.body)
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
- const payload = text ? JSON.parse(text) : null;
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 ? { ...config, apiUrl: String(input.target_base_url).replace(/\/+$/, "") } : config;
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 results = [
420
- { test: "health", success: false, details: null },
421
- { test: "ready", success: false, details: null },
422
- { test: "local_mode_contract", success: false, details: null }
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
- results.push({
432
- test: "auth_methods",
433
- success: true,
434
- details: await targetClient.get("/api/auth/methods")
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: true,
439
- summary: `${results.filter((result) => result.success).length}/${results.length} tests passed`,
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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  createSpapsMcpServer
4
- } from "./chunk-GPBTYWDD.js";
4
+ } from "./chunk-WWRSSH7A.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
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
@@ -10,7 +10,7 @@ import {
10
10
  listWizardSteps,
11
11
  resolveConfig,
12
12
  resolveWizardDir
13
- } from "./chunk-GPBTYWDD.js";
13
+ } from "./chunk-WWRSSH7A.js";
14
14
  export {
15
15
  SpapsHttpClient,
16
16
  SpapsHttpError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spaps-mcp",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Model Context Protocol server for SPAPS docs, contracts, capability checks, and integration wizard steps",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",