Skip to content

Commit 83b86f7

Browse files
fix(mcp): close P1/P2 ledger findings — README drift, server.json scope, sdk pin, create_vector tool (#13)
Squash-merged via triage. Rebased onto master (after #12 + #11). Added test fixture fixes: EXPECTED_TOOLS 16→17, /vector/new route in mock. Closes B16-F7 (README), F8 (server.json), F9 (SDK pin), F10 (create_vector).
1 parent c2b03dd commit 83b86f7

9 files changed

Lines changed: 140 additions & 11 deletions

File tree

README.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ to reach for this MCP, see <https://instanode.dev/agent.html>.
107107
| Tool | Description |
108108
|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
109109
| `create_postgres` | `POST /db/new` — Provision a Postgres database (pgvector included). Returns `connection_url` + the `note`/`upgrade` claim URL. `name` required. |
110+
| `create_vector` | `POST /vector/new` — Provision a pgvector-enabled Postgres database (embedding store). Returns `connection_url` + `extension`/`dimensions` + `note`/`upgrade`. `name` required; optional `dimensions` is a documentation hint. |
110111
| `create_cache` | `POST /cache/new` — Provision a Redis cache (ACL-scoped user + namespace). Returns `connection_url` + `note`/`upgrade`. `name` required. |
111112
| `create_nosql` | `POST /nosql/new` — Provision a MongoDB database (per-resource user + DB-scoped role). Returns `connection_url` + `note`/`upgrade`. `name` required. |
112113
| `create_queue` | `POST /queue/new` — Provision a NATS JetStream queue (scoped subject namespace). Returns `connection_url` + `note`/`upgrade`. `name` required. |
@@ -118,10 +119,10 @@ to reach for this MCP, see <https://instanode.dev/agent.html>.
118119
| `redeploy` | `POST /deploy/:id/redeploy` — Rebuild + rolling update an existing deployment. Requires `INSTANODE_TOKEN`. |
119120
| `delete_deployment` | `DELETE /deploy/:id` — Tear down a running deployment. Irreversible. Requires `INSTANODE_TOKEN`. |
120121
| `claim_resource` | Helper — turn an `upgrade_jwt` from any `create_*` response into the dashboard claim URL the user should click. No API call. No auth required. |
121-
| `claim_token` | `POST /api/me/claim` — Programmatic claim: attach an anonymous resource to the authenticated account by its UUID `token`. Requires `INSTANODE_TOKEN`. |
122-
| `list_resources` | `GET /api/me/resources` — List resources on the caller's account. Requires `INSTANODE_TOKEN`. |
123-
| `delete_resource` | `DELETE /api/me/resources/{token}` — Hard-delete a resource you own. Paid tier only. Requires `INSTANODE_TOKEN`. |
124-
| `get_api_token` | `GET /api/me/token` — Mint a fresh 30-day bearer JWT (for rotation). Requires an existing `INSTANODE_TOKEN`. |
122+
| `claim_token` | `POST /claim` — Programmatic claim: attach an anonymous resource to the authenticated account using its `upgrade_jwt` + `email`. No auth required. |
123+
| `list_resources` | `GET /api/v1/resources` — List resources on the caller's account. Requires `INSTANODE_TOKEN`. |
124+
| `delete_resource` | `DELETE /api/v1/resources/{token}` — Hard-delete a resource you own. Paid tier only. Requires `INSTANODE_TOKEN`. |
125+
| `get_api_token` | `POST /api/v1/auth/api-keys` — Mint a fresh bearer Personal Access Token (PAT). Requires an existing user-session `INSTANODE_TOKEN` (PATs cannot mint other PATs — the API returns 403 in that case). |
125126

126127
### Container deployment (`create_deploy`)
127128

@@ -290,7 +291,7 @@ resources, and the account-management tools (`list_resources`,
290291
3. Set it as `INSTANODE_TOKEN` in the MCP server's `env` block (see examples
291292
above).
292293

293-
Rotate any time by calling `get_api_token`, which mints a fresh 30-day JWT.
294+
Rotate any time by calling `get_api_token`, which mints a fresh Personal Access Token via `POST /api/v1/auth/api-keys`. PATs are revocation-based (not time-bound). NOTE: PATs cannot mint other PATs — `get_api_token` requires a user-session token (sign in via the dashboard), not an existing PAT, otherwise the API returns 403.
294295

295296
## Development
296297

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"prepublishOnly": "npm run build"
5252
},
5353
"dependencies": {
54-
"@modelcontextprotocol/sdk": "^1.10.2"
54+
"@modelcontextprotocol/sdk": "1.29.0"
5555
},
5656
"devDependencies": {
5757
"@types/node": "^22.10.2",

server.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
33
"name": "io.github.InstaNode-dev/mcp",
4-
"description": "Provision Postgres databases + webhooks from AI coding agents in one HTTP call.",
4+
"description": "Provision Postgres (with pgvector), Redis, MongoDB, NATS JetStream queues, S3-compatible object storage, inbound webhook receivers, and deploy containerized apps from AI coding agents in single HTTP calls.",
55
"repository": {
66
"url": "https://github.com/InstaNode-dev/mcp",
77
"source": "github"

src/client.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,18 @@ export interface DatabaseProvisionResult extends ProvisionResultBase {
128128
connection_url: string;
129129
}
130130

131+
export interface VectorProvisionResult extends ProvisionResultBase {
132+
/**
133+
* postgres:// connection string with the pgvector extension pre-installed
134+
* (CREATE EXTENSION vector already ran). Drop-in DATABASE_URL.
135+
*/
136+
connection_url: string;
137+
/** Always 'pgvector'. */
138+
extension?: string;
139+
/** Echo of the requested default embedding dimensions hint (defaults to 1536). */
140+
dimensions?: number;
141+
}
142+
131143
export interface CacheProvisionResult extends ProvisionResultBase {
132144
/** redis://user:pass@host:port — drop-in REDIS_URL. */
133145
connection_url: string;
@@ -599,6 +611,22 @@ export class InstantClient {
599611
return this.request<DatabaseProvisionResult>("POST", "/db/new", { name });
600612
}
601613

614+
/**
615+
* POST /vector/new — provision a pgvector-enabled Postgres database. `name`
616+
* is required client-side for parity with the other create_* tools (the
617+
* server allows it to be omitted, but every other endpoint requires it).
618+
* Optional `dimensions` is a documentation hint only — pgvector picks
619+
* dimensions per column at table-create time.
620+
*/
621+
async createVector(
622+
name: string,
623+
dimensions?: number
624+
): Promise<VectorProvisionResult> {
625+
const body: { name: string; dimensions?: number } = { name };
626+
if (typeof dimensions === "number") body.dimensions = dimensions;
627+
return this.request<VectorProvisionResult>("POST", "/vector/new", body);
628+
}
629+
602630
/** POST /cache/new — provision a Redis cache. `name` is required. */
603631
async createCache(name: string): Promise<CacheProvisionResult> {
604632
return this.request<CacheProvisionResult>("POST", "/cache/new", { name });

src/index.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* Exposes tools to AI coding agents (Claude Code, Cursor, Windsurf, etc.):
66
*
77
* create_postgres — provision an ephemeral Postgres database (with pgvector)
8+
* create_vector — provision a pgvector-enabled Postgres database (embedding store)
89
* create_cache — provision a Redis cache (ACL-scoped user + namespace)
910
* create_nosql — provision a MongoDB database (per-resource user + role)
1011
* create_queue — provision a NATS JetStream queue (publish/subscribe)
@@ -238,6 +239,75 @@ Store the connection_url in an env var (DATABASE_URL); do not hardcode it.`,
238239
}
239240
);
240241

242+
// ── Tool: create_vector ───────────────────────────────────────────────────────
243+
244+
server.tool(
245+
"create_vector",
246+
`Provision a pgvector-enabled Postgres database on instanode.dev (POST /vector/new).
247+
248+
Underlying storage IS Postgres (tier limits mirror Postgres exactly), with the
249+
pgvector extension already loaded via CREATE EXTENSION vector at provisioning
250+
time. Use for embedding stores (OpenAI text-embedding-ada-002 / 3-small = 1536
251+
dims; text-embedding-3-large = 3072). Returns a standard postgres:// connection
252+
URL — drop in as DATABASE_URL with any pg driver.
253+
254+
Note: create_postgres ALSO ships with pgvector pre-installed today, so this
255+
tool is functionally equivalent for embedding workloads. Use create_vector when
256+
the agent wants to make the intent (pgvector / embeddings) explicit, or when
257+
the API contract evolves and pgvector-only routing diverges from generic
258+
Postgres provisioning.
259+
260+
The optional 'dimensions' field is a documentation hint only — pgvector lets
261+
you pick per-column dimensions at table-create time, so the server stores the
262+
declared default but does not enforce it.
263+
264+
Without INSTANODE_TOKEN: anonymous tier — 10 MB, 2 connections, expires in 24h,
265+
capped at 5 provisions/day per /24 subnet. The response carries 'note' +
266+
'upgrade' (claim URL) — surface both verbatim.
267+
With INSTANODE_TOKEN (paid): hobby/pro/team Postgres limits, permanent.
268+
269+
The 'name' field is required.`,
270+
{
271+
...nameArg,
272+
dimensions: z
273+
.number()
274+
.int()
275+
.min(1)
276+
.max(16000)
277+
.optional()
278+
.describe(
279+
"Optional embedding dimension hint (defaults to 1536 — OpenAI text-embedding-3-small / ada-002). Use 3072 for text-embedding-3-large. Informational only; pgvector enforces dimensions per column at table-create time."
280+
),
281+
},
282+
async ({ name, dimensions }) => {
283+
try {
284+
const result = await client.createVector(name, dimensions);
285+
const lines = [
286+
`pgvector Postgres database provisioned.`,
287+
`Token: ${result.token}`,
288+
`Name: ${result.name ?? name}`,
289+
`Tier: ${result.tier}`,
290+
`Connection URL: ${result.connection_url}`,
291+
`Extension: ${result.extension ?? "pgvector"}`,
292+
`Dimensions: ${result.dimensions ?? dimensions ?? 1536}`,
293+
...formatLimits(result.limits),
294+
];
295+
appendUpgradeBlock(lines, result);
296+
lines.push(
297+
``,
298+
`Use directly as DATABASE_URL (add .env to .gitignore):`,
299+
` DATABASE_URL=${result.connection_url}`,
300+
``,
301+
`pgvector is ready — no CREATE EXTENSION needed. Pick column dimensions`,
302+
`at CREATE TABLE time, e.g. embedding vector(1536).`
303+
);
304+
return textResult(lines.join("\n"));
305+
} catch (err) {
306+
return textResult(formatError(err));
307+
}
308+
}
309+
);
310+
241311
// ── Tool: create_cache ────────────────────────────────────────────────────────
242312

243313
server.tool(

test.sh

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ import sys, json
3434
d = json.loads(sys.stdin.read())
3535
tools = {t['name'] for t in d['result']['tools']}
3636
expected = {
37-
'create_postgres', 'create_cache', 'create_nosql', 'create_queue',
37+
'create_postgres', 'create_vector', 'create_cache', 'create_nosql', 'create_queue',
3838
'create_storage', 'create_webhook',
3939
'create_deploy', 'list_deployments', 'get_deployment',
4040
'redeploy', 'delete_deployment',
@@ -52,7 +52,7 @@ assert not still_there, f'dead tools still registered: {still_there}'
5252
extra = tools - expected
5353
assert not extra, f'unexpected tools registered: {extra}'
5454
" || fail "tools/list missing expected tools or carrying dead ones"
55-
pass "tools/list returns all 16 tools, no dead ones"
55+
pass "tools/list returns all 17 tools, no dead ones"
5656

5757
# Test 2b: tools/list — deploy management tools are registered and discoverable.
5858
# Explicit assertion so the smoke test catches a regression on any single one.
@@ -89,6 +89,34 @@ assert err, f'expected a validation error, got: {d}'
8989
" || fail "create_postgres with empty name was accepted"
9090
pass "create_postgres rejects empty name"
9191

92+
# Test 4b: create_vector (B16-F10) is registered and rejects an empty name.
93+
# Also verifies the optional dimensions field is advertised in the schema.
94+
BAD_VECTOR='{"jsonrpc":"2.0","id":40,"method":"tools/call","params":{"name":"create_vector","arguments":{"name":""}}}'
95+
RESP=$(printf "%s\n%s\n" "$INIT" "$BAD_VECTOR" | INSTANODE_API_URL="$BASE_URL" $MCP 2>/dev/null | tail -1)
96+
echo "$RESP" | python3 -c "
97+
import sys, json
98+
d = json.loads(sys.stdin.read())
99+
err = d.get('error') or (d.get('result') or {}).get('isError')
100+
assert err, f'expected a validation error, got: {d}'
101+
" || fail "create_vector with empty name was accepted"
102+
pass "create_vector rejects empty name"
103+
104+
# Test 4c: create_vector schema advertises optional dimensions
105+
RESP=$(printf "%s\n%s\n" "$INIT" "$TOOLS_LIST" | INSTANODE_API_URL="$BASE_URL" $MCP 2>/dev/null | tail -1)
106+
echo "$RESP" | python3 -c "
107+
import sys, json
108+
d = json.loads(sys.stdin.read())
109+
tools = {t['name']: t for t in d['result']['tools']}
110+
assert 'create_vector' in tools, f'create_vector not registered'
111+
schema = tools['create_vector']['inputSchema']
112+
props = schema.get('properties', {})
113+
assert 'name' in props, f'create_vector missing name: {list(props.keys())}'
114+
assert 'dimensions' in props, f'create_vector missing dimensions: {list(props.keys())}'
115+
desc = tools['create_vector'].get('description', '')
116+
assert 'pgvector' in desc.lower(), f'create_vector description should mention pgvector'
117+
" || fail "create_vector schema missing expected properties"
118+
pass "create_vector schema advertises name + dimensions and mentions pgvector"
119+
92120
# Test 5: claim_resource is a pure helper — works without any API/network access.
93121
# Accepts a raw JWT and builds the dashboard claim URL.
94122
CLAIM='{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"claim_resource","arguments":{"upgrade_jwt":"ey.fake.jwt"}}}'

test/integration.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ const SERVER_ENTRY = resolve(__dirname, "..", "..", "dist", "index.js");
5454
/** Every tool the server is contractually required to register. */
5555
const EXPECTED_TOOLS = [
5656
"create_postgres",
57+
"create_vector",
5758
"create_cache",
5859
"create_nosql",
5960
"create_queue",
@@ -200,7 +201,7 @@ describe("instanode-mcp integration suite", () => {
200201
// ── Tool registry + schemas ─────────────────────────────────────────────────
201202

202203
describe("tool registry", () => {
203-
it("registers exactly the 16 contract tools, no dead ones", async () => {
204+
it("registers exactly the 17 contract tools, no dead ones", async () => {
204205
const { client, close } = await connectClient(mock.url, "none");
205206
try {
206207
const { tools } = await client.listTools();

test/mock-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,7 @@ async function route(req: IncomingMessage, res: ServerResponse, state: State): P
331331
// ── create_* provisioning routes ──────────────────────────────────────────
332332
const provisionRoutes: Record<string, string> = {
333333
"/db/new": "postgres",
334+
"/vector/new": "postgres",
334335
"/cache/new": "cache",
335336
"/nosql/new": "nosql",
336337
"/queue/new": "queue",

0 commit comments

Comments
 (0)