Skip to content

Commit d1edab1

Browse files
fix(mcp): close regressions of T17 tasks 170,173,174,176,171 — empty 2xx + zod regex + audit + UA + PAT clarity (#12)
Squash-merged via triage. Closes regressions T17 tasks 170,173,174,176,171 on master directly (wave2+wave3 were never merged). 62/62 tests pass.
1 parent 3b5ddb9 commit d1edab1

11 files changed

Lines changed: 2322 additions & 57 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
node_modules/
22
dist/
3+
dist-test/

package-lock.json

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "instanode-mcp",
3-
"version": "0.11.0",
3+
"version": "0.11.1",
44
"description": "MCP server for instanode.dev \u2014 lets AI coding agents provision ephemeral Postgres, Redis, MongoDB, NATS queues, S3-compatible object storage, webhook receivers, and deploy containerized apps over HTTPS, with optional bearer-token auth for paid users.",
55
"keywords": [
66
"mcp",
@@ -45,7 +45,9 @@
4545
"build": "tsc",
4646
"dev": "tsc --watch",
4747
"start": "node dist/index.js",
48-
"test": "bash test.sh",
48+
"pretest": "tsc && tsc -p tsconfig.test.json",
49+
"test": "node --test dist-test/test/integration.test.js dist-test/test/live-smoke.test.js",
50+
"test:smoke": "bash test.sh",
4951
"prepublishOnly": "npm run build"
5052
},
5153
"dependencies": {

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
"url": "https://github.com/InstaNode-dev/mcp",
77
"source": "github"
88
},
9-
"version": "0.11.0",
9+
"version": "0.11.1",
1010
"websiteUrl": "https://instanode.dev",
1111
"packages": [
1212
{
1313
"registryType": "npm",
1414
"identifier": "instanode-mcp",
15-
"version": "0.11.0",
15+
"version": "0.11.1",
1616
"transport": {
1717
"type": "stdio"
1818
},

src/client.ts

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,39 @@
3131
* client to the canonical routes above.
3232
*/
3333

34+
import { readFileSync } from "node:fs";
35+
import { dirname, resolve } from "node:path";
36+
import { fileURLToPath } from "node:url";
37+
3438
const DEFAULT_BASE_URL = "https://api.instanode.dev";
3539
const DEFAULT_DASHBOARD_URL = "https://instanode.dev";
3640

41+
/**
42+
* Module-init User-Agent — resolved once from package.json so every release
43+
* naturally rolls forward without anyone remembering to bump a hardcoded
44+
* string. BugBash B16 F4 (regression of task #176): the previous version
45+
* carried `instanode-mcp/0.11.0` as a literal in two places, which drifted
46+
* out of sync with the published package version. Falling back to "dev" on
47+
* any failure (file missing, malformed JSON) so the client never explodes
48+
* at import time just to read a UA.
49+
*/
50+
function resolveUserAgent(): string {
51+
try {
52+
const here = dirname(fileURLToPath(import.meta.url));
53+
// dist/client.js → repo root; src/client.ts → repo root in dev. Same path.
54+
const pkgPath = resolve(here, "..", "package.json");
55+
const pkgRaw = readFileSync(pkgPath, "utf8");
56+
const pkg = JSON.parse(pkgRaw) as { name?: string; version?: string };
57+
const name = pkg.name && pkg.name.length > 0 ? pkg.name : "instanode-mcp";
58+
const version = pkg.version && pkg.version.length > 0 ? pkg.version : "dev";
59+
return `${name}/${version}`;
60+
} catch {
61+
return "instanode-mcp/dev";
62+
}
63+
}
64+
65+
const USER_AGENT = resolveUserAgent();
66+
3767
export interface ClientOptions {
3868
baseURL?: string;
3969
}
@@ -200,6 +230,23 @@ export interface DeployDeleteResult {
200230
message?: string;
201231
}
202232

233+
/**
234+
* Response shape from POST /deploy/:id/redeploy.
235+
*
236+
* The live API documents this as a bare 202 with NO body (see openapi.json),
237+
* not a deployment record. The previous client mis-typed it as DeployGetResult
238+
* and the index.ts handler dereferenced `result.item.app_id`, blowing up
239+
* with "Cannot read properties of undefined (reading 'app_id')" on every
240+
* real call. BugBash B16 F1 (regression of task #170): use a body-less type
241+
* and let callers fall back to the caller-supplied id when needed.
242+
*/
243+
export interface RedeployResult {
244+
ok: boolean;
245+
id?: string;
246+
status?: string;
247+
message?: string;
248+
}
249+
203250
/** Caller-supplied params for create_deploy. */
204251
export interface CreateDeployParams {
205252
/** Base64-encoded gzip tarball (with Dockerfile + source). <50 MB after decode. */
@@ -360,7 +407,7 @@ export class InstantClient {
360407
private headers(): Record<string, string> {
361408
const h: Record<string, string> = {
362409
"Content-Type": "application/json",
363-
"User-Agent": "instanode-mcp/0.11.0",
410+
"User-Agent": USER_AGENT,
364411
};
365412
const tok = this.bearerToken();
366413
if (tok) {
@@ -375,7 +422,7 @@ export class InstantClient {
375422
*/
376423
private authHeaders(): Record<string, string> {
377424
const h: Record<string, string> = {
378-
"User-Agent": "instanode-mcp/0.11.0",
425+
"User-Agent": USER_AGENT,
379426
};
380427
const tok = this.bearerToken();
381428
if (tok) {
@@ -444,6 +491,19 @@ export class InstantClient {
444491
);
445492
}
446493

494+
// BugBash B16 F1 (regression of task #170 P0-1): empty 2xx bodies used to
495+
// leave `data` as undefined, and any caller that did `result.foo` blew up
496+
// with "Cannot read properties of undefined (reading 'foo')". Eight tools
497+
// hit this: redeploy, delete_resource, delete_deployment, get_deployment,
498+
// list_deployments, list_resources, get_api_token, claim_token — most
499+
// commonly redeploy + delete_*, which the API documents as bare 2xx (no
500+
// body). The previous fix only patched redeploy. Now: any 2xx with an
501+
// empty body returns a safe sentinel `{ok: true}` so the dereferencing
502+
// path stays alive. Callers that need richer fields handle the empty
503+
// case explicitly (see redeploy / deleteResource / deleteDeployment).
504+
if (data === undefined) {
505+
return { ok: true } as T;
506+
}
447507
return data as T;
448508
}
449509

@@ -510,6 +570,13 @@ export class InstantClient {
510570
);
511571
}
512572

573+
// Same empty-2xx safe sentinel as request<T>(). See the long comment up
574+
// there for the why. requestMultipart() is only used for create_deploy
575+
// today, which always returns a JSON body, but mirroring the safety
576+
// makes the two paths drift-free.
577+
if (data === undefined) {
578+
return { ok: true } as T;
579+
}
513580
return data as T;
514581
}
515582

@@ -715,14 +782,31 @@ export class InstantClient {
715782
);
716783
}
717784

718-
/** POST /deploy/:id/redeploy — rebuild + rolling update an existing app. */
719-
async redeploy(id: string): Promise<DeployGetResult> {
720-
return this.request<DeployGetResult>(
785+
/**
786+
* POST /deploy/:id/redeploy — rebuild + rolling update an existing app.
787+
*
788+
* The live API returns a bare 202 with no body (see openapi.json). Earlier
789+
* versions of this client typed the response as DeployGetResult and the
790+
* tool handler dereferenced `result.item.app_id`, throwing
791+
* "Cannot read properties of undefined (reading 'app_id')" on every real
792+
* call. BugBash B16 F1 (regression of task #170): the empty-body now
793+
* resolves to `{ok: true}` via the request<T>() empty-2xx sentinel; this
794+
* helper layers the caller-supplied id on top so the tool handler has a
795+
* stable surface to read.
796+
*/
797+
async redeploy(id: string): Promise<RedeployResult> {
798+
const raw = await this.request<RedeployResult>(
721799
"POST",
722800
`/deploy/${encodeURIComponent(id)}/redeploy`,
723801
undefined,
724802
{ requireAuth: true }
725803
);
804+
return {
805+
ok: raw.ok ?? true,
806+
id: raw.id ?? id,
807+
status: raw.status ?? "building",
808+
message: raw.message,
809+
};
726810
}
727811

728812
/** DELETE /deploy/:id — tear down the running pod + remove the record. */

0 commit comments

Comments
 (0)