From 4349b85020df0486273604fba489bea08c96d66e Mon Sep 17 00:00:00 2001 From: Harshal Patil <12152047+harche@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:19:50 -0500 Subject: [PATCH 1/5] feat: re-create reaped sandboxes transparently on getClient When a sandbox is reaped due to idle timeout, re-create it automatically instead of throwing an error. Also adds postgres example and k8s manifest. Co-Authored-By: Claude Opus 4.6 (1M context) --- examples/prodisco.postgres-real.yaml | 9 ++ k8s/mcp-server-postgres.yaml | 207 ++++++++++++++++++++++++++ src/__tests__/sandbox-manager.test.ts | 14 +- src/sandbox-manager.ts | 10 +- 4 files changed, 232 insertions(+), 8 deletions(-) create mode 100644 examples/prodisco.postgres-real.yaml create mode 100644 k8s/mcp-server-postgres.yaml diff --git a/examples/prodisco.postgres-real.yaml b/examples/prodisco.postgres-real.yaml new file mode 100644 index 0000000..7efd0ec --- /dev/null +++ b/examples/prodisco.postgres-real.yaml @@ -0,0 +1,9 @@ +libraries: + - name: "pg" + description: | + PostgreSQL client for Node.js. Connect to the PostgreSQL server running in the cluster. + Quick start: `const { Client } = require("pg"); const client = new Client({ host: "postgresql.prodisco.svc.cluster.local", port: 5432, user: "prodisco", password: "prodisco", database: "prodisco" }); await client.connect();` + Queries: `const res = await client.query("SELECT * FROM my_table");` returns `{ rows, rowCount, fields }`. + Parameterized: `await client.query("INSERT INTO users(name, age) VALUES($1, $2)", ["alice", 30]);` + Always call `await client.end();` when done. + IMPORTANT: Before querying or modifying any table, ALWAYS discover the schema first. List tables with `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'` and inspect columns with `SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'my_table'`. Never assume column names or types exist — verify them first. diff --git a/k8s/mcp-server-postgres.yaml b/k8s/mcp-server-postgres.yaml new file mode 100644 index 0000000..fe7f10f --- /dev/null +++ b/k8s/mcp-server-postgres.yaml @@ -0,0 +1,207 @@ +--- +apiVersion: v1 +kind: Namespace +metadata: + name: prodisco +--- +# PostgreSQL database +apiVersion: v1 +kind: Pod +metadata: + name: postgresql + namespace: prodisco + labels: + app: postgresql +spec: + containers: + - name: postgresql + image: postgres:16 + ports: + - containerPort: 5432 + protocol: TCP + env: + - name: POSTGRES_USER + value: "prodisco" + - name: POSTGRES_PASSWORD + value: "prodisco" + - name: POSTGRES_DB + value: "prodisco" + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "500m" +--- +apiVersion: v1 +kind: Service +metadata: + name: postgresql + namespace: prodisco + labels: + app: postgresql +spec: + type: ClusterIP + ports: + - port: 5432 + targetPort: 5432 + protocol: TCP + name: postgres + selector: + app: postgresql +--- +# ConfigMap with ProDisco configuration +apiVersion: v1 +kind: ConfigMap +metadata: + name: prodisco-config + namespace: prodisco +data: + .prodisco-config.yaml: | + libraries: + - name: "pg" + description: | + PostgreSQL client for Node.js. Connect to the PostgreSQL server running in the cluster. + Quick start: `const { Client } = require("pg"); const client = new Client({ host: "postgresql.prodisco.svc.cluster.local", port: 5432, user: "prodisco", password: "prodisco", database: "prodisco" }); await client.connect();` + Queries: `const res = await client.query("SELECT * FROM my_table");` returns `{ rows, rowCount, fields }`. + Parameterized: `await client.query("INSERT INTO users(name, age) VALUES($1, $2)", ["alice", 30]);` + Always call `await client.end();` when done. + IMPORTANT: Before querying or modifying any table, ALWAYS discover the schema first. List tables with `SELECT table_name FROM information_schema.tables WHERE table_schema = 'public'` and inspect columns with `SELECT column_name, data_type FROM information_schema.columns WHERE table_name = 'my_table'`. Never assume column names or types exist — verify them first. +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mcp-server + namespace: prodisco +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: sandbox-server + namespace: prodisco +--- +# RBAC for MCP server to manage Sandbox CRDs (multi-sandbox mode) +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: mcp-server-sandbox-manager +rules: + - apiGroups: ["agents.x-k8s.io"] + resources: + - sandboxes + verbs: ["get", "list", "create", "delete", "watch"] + - apiGroups: ["agents.x-k8s.io"] + resources: + - sandboxes/status + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: mcp-server-sandbox-manager +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: mcp-server-sandbox-manager +subjects: + - kind: ServiceAccount + name: mcp-server + namespace: prodisco +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mcp-server + namespace: prodisco + labels: + app: mcp-server +spec: + replicas: 1 + selector: + matchLabels: + app: mcp-server + template: + metadata: + labels: + app: mcp-server + spec: + serviceAccountName: mcp-server + containers: + - name: mcp-server + image: prodisco/mcp-server:test + imagePullPolicy: IfNotPresent + ports: + - containerPort: 3000 + name: http + protocol: TCP + env: + - name: MCP_TRANSPORT + value: "http" + - name: MCP_HOST + value: "0.0.0.0" + - name: MCP_PORT + value: "3000" + - name: SCRIPTS_CACHE_DIR + value: "/tmp/prodisco-scripts" + - name: PRODISCO_ENABLE_APPS + value: "true" + # Multi-sandbox mode: each MCP session gets its own Sandbox CRD + - name: SANDBOX_MODE + value: "multi" + - name: SANDBOX_NAMESPACE + value: "prodisco" + - name: SANDBOX_TCP_PORT + value: "50051" + - name: SANDBOX_IMAGE + value: "prodisco/sandbox-server:test" + - name: PRODISCO_CONFIG_PATH + value: "/config/.prodisco-config.yaml" + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "1Gi" + cpu: "1000m" + readinessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 10 + periodSeconds: 5 + livenessProbe: + httpGet: + path: /health + port: 3000 + initialDelaySeconds: 15 + periodSeconds: 30 + volumeMounts: + - name: scripts-cache + mountPath: /tmp/prodisco-scripts + - name: prodisco-config + mountPath: /config + readOnly: true + volumes: + - name: scripts-cache + emptyDir: {} + - name: prodisco-config + configMap: + name: prodisco-config +--- +apiVersion: v1 +kind: Service +metadata: + name: mcp-server + namespace: prodisco + labels: + app: mcp-server +spec: + type: ClusterIP + ports: + - port: 3000 + targetPort: 3000 + protocol: TCP + name: http + selector: + app: mcp-server diff --git a/src/__tests__/sandbox-manager.test.ts b/src/__tests__/sandbox-manager.test.ts index 2a33829..942b34b 100644 --- a/src/__tests__/sandbox-manager.test.ts +++ b/src/__tests__/sandbox-manager.test.ts @@ -109,15 +109,17 @@ describe('SandboxManager - Multi Mode Session Tracking', () => { await manager.shutdown(); }); - it('rejects getClient for unknown session', async () => { - const manager = new SandboxManager({ mode: 'multi' }); + it('rejects getClient for unknown session (re-creation fails without K8s)', async () => { + const manager = new SandboxManager({ + mode: 'multi', + readyTimeoutMs: 1000, + }); - await expect(manager.getClient('nonexistent')).rejects.toThrow( - 'no sandbox for session nonexistent', - ); + // Without a real sandbox pod, re-creation attempt will fail + await expect(manager.getClient('nonexistent')).rejects.toThrow(); await manager.shutdown(); - }); + }, 10000); it('onSessionClosed is idempotent', async () => { const manager = new SandboxManager({ mode: 'multi' }); diff --git a/src/sandbox-manager.ts b/src/sandbox-manager.ts index 000b9bb..56e15ad 100644 --- a/src/sandbox-manager.ts +++ b/src/sandbox-manager.ts @@ -122,9 +122,15 @@ export class SandboxManager { await pending; } - const session = this.sessions.get(sessionId); + let session = this.sessions.get(sessionId); if (!session) { - throw new Error(`SandboxManager: no sandbox for session ${sessionId}`); + // Sandbox was reaped due to idle timeout — re-create it transparently + logger.info(`Re-creating sandbox for session ${sessionId} (previously reaped)`); + await this.onSessionInitialized(sessionId); + session = this.sessions.get(sessionId); + if (!session) { + throw new Error(`SandboxManager: failed to re-create sandbox for session ${sessionId}`); + } } session.lastActivityMs = Date.now(); From 3dbfe3818407bff4fc92e6c04661863879f63db4 Mon Sep 17 00:00:00 2001 From: Harshal Patil <12152047+harche@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:26:48 -0500 Subject: [PATCH 2/5] ci: add workflow_dispatch to publish workflow Allow manual triggering of the publish workflow for releasing the root package when no workspace packages have changed. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d6dbe25..301524a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,6 +4,7 @@ on: push: branches: - main + workflow_dispatch: concurrency: ${{ github.workflow }}-${{ github.ref }} From fdd99aa6a796d5538999e8a955b4bde469367aa9 Mon Sep 17 00:00:00 2001 From: Harshal Patil <12152047+harche@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:37:13 -0500 Subject: [PATCH 3/5] ci: include root package in changesets workflow Add root directory to workspaces so changesets manages @prodisco/mcp-server directly. Remove redundant manual version bump and publish from CI scripts. Add workflow_dispatch trigger and changeset for minor release. Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/plenty-tables-talk.md | 5 +++++ package-lock.json | 17 +++++++++++------ package.json | 1 + scripts/ci/changeset-publish.sh | 1 - scripts/ci/changeset-version.sh | 1 - 5 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 .changeset/plenty-tables-talk.md diff --git a/.changeset/plenty-tables-talk.md b/.changeset/plenty-tables-talk.md new file mode 100644 index 0000000..45ea1f3 --- /dev/null +++ b/.changeset/plenty-tables-talk.md @@ -0,0 +1,5 @@ +--- +"@prodisco/mcp-server": minor +--- + +Re-create reaped sandboxes transparently on getClient instead of throwing an error diff --git a/package-lock.json b/package-lock.json index 6007fe2..b7d6d53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.12", "license": "MIT", "workspaces": [ + ".", "packages/search-libs", "packages/loki-client", "packages/prometheus-client", @@ -2049,6 +2050,10 @@ "resolved": "packages/loki-client", "link": true }, + "node_modules/@prodisco/mcp-server": { + "resolved": "", + "link": true + }, "node_modules/@prodisco/prometheus-client": { "resolved": "packages/prometheus-client", "link": true @@ -6974,7 +6979,7 @@ }, "packages/loki-client": { "name": "@prodisco/loki-client", - "version": "0.1.2", + "version": "0.1.3", "devDependencies": { "@types/node": "^24.10.1", "typescript": "^5.9.3", @@ -6983,7 +6988,7 @@ }, "packages/prometheus-client": { "name": "@prodisco/prometheus-client", - "version": "0.1.2", + "version": "0.1.3", "license": "ISC", "dependencies": { "@prodisco/search-libs": "*", @@ -6997,13 +7002,13 @@ }, "packages/sandbox-server": { "name": "@prodisco/sandbox-server", - "version": "0.1.4", + "version": "0.1.5", "dependencies": { "@bufbuild/protobuf": "^2.2.3", "@grpc/grpc-js": "^1.12.5", "@kubernetes/client-node": "^1.4.0", - "@prodisco/loki-client": "^0.1.2", - "@prodisco/prometheus-client": "^0.1.2", + "@prodisco/loki-client": "^0.1.3", + "@prodisco/prometheus-client": "^0.1.3", "esbuild": "^0.27.1", "prometheus-query": "^3.3.2", "simple-statistics": "^7.8.8" @@ -7022,7 +7027,7 @@ }, "packages/search-libs": { "name": "@prodisco/search-libs", - "version": "0.1.2", + "version": "0.1.3", "license": "MIT", "dependencies": { "@orama/orama": "^3.1.5", diff --git a/package.json b/package.json index 0ac2d24..6760006 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "main": "dist/server.js", "workspaces": [ + ".", "packages/search-libs", "packages/loki-client", "packages/prometheus-client", diff --git a/scripts/ci/changeset-publish.sh b/scripts/ci/changeset-publish.sh index b0c5764..237e049 100755 --- a/scripts/ci/changeset-publish.sh +++ b/scripts/ci/changeset-publish.sh @@ -1,4 +1,3 @@ #!/bin/bash set -e npx changeset publish -npm publish || true diff --git a/scripts/ci/changeset-version.sh b/scripts/ci/changeset-version.sh index 45a2967..9c346d4 100755 --- a/scripts/ci/changeset-version.sh +++ b/scripts/ci/changeset-version.sh @@ -1,4 +1,3 @@ #!/bin/bash set -e npx changeset version -npm version patch --no-git-tag-version From 0dc3788cc908d9561fd904673560418c938c2480 Mon Sep 17 00:00:00 2001 From: Harshal Patil <12152047+harche@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:39:52 -0500 Subject: [PATCH 4/5] fix: change changeset bump type from minor to patch Co-Authored-By: Claude Opus 4.6 (1M context) --- .changeset/plenty-tables-talk.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/plenty-tables-talk.md b/.changeset/plenty-tables-talk.md index 45ea1f3..e377565 100644 --- a/.changeset/plenty-tables-talk.md +++ b/.changeset/plenty-tables-talk.md @@ -1,5 +1,5 @@ --- -"@prodisco/mcp-server": minor +"@prodisco/mcp-server": patch --- Re-create reaped sandboxes transparently on getClient instead of throwing an error From cde3305d54b118b318b1bff2b3ec70867421b82a Mon Sep 17 00:00:00 2001 From: Harshal Patil <12152047+harche@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:47:14 -0500 Subject: [PATCH 5/5] fix: make husky prepare script resilient for Docker builds Adding root to workspaces causes npm to run the prepare script even with --ignore-scripts in some cases. Use "husky || true" so it succeeds gracefully when husky is not installed (e.g. production images). Co-Authored-By: Claude Opus 4.6 (1M context) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6760006..6c86810 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "test": "vitest run", "test:integration": "scripts/integration/run-kind-integration.sh", "test:examples": "bash scripts/ci/examples-smoke.sh", - "prepare": "husky" + "prepare": "husky || true" }, "keywords": [ "kubernetes",