From 8f9fe32ec2ad1d4c8473e1b49bdf4003df31cb7e Mon Sep 17 00:00:00 2001 From: Orca Date: Mon, 10 Aug 2026 11:09:41 +0800 Subject: [PATCH] =?UTF-8?q?chore(scripts):=20mcp-probe=20=E2=80=94=20drive?= =?UTF-8?q?=20oab-mcp=20directly,=20no=20desktop=20app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tiny stdio driver for the oab-mcp core: initialize → tools/call, printing raw JSON-RPC and forwarding the core's stderr. Lets you debug credential / region / tool behavior in a fast terminal loop instead of build → download → click the .app. Point it at the core bundled in the shipped .app and vary AWS_PROFILE/AWS_REGION to reproduce credential drift in one shot. Co-Authored-By: Claude Opus 4.8 --- scripts/README.md | 22 +++++++++ scripts/mcp-probe.mjs | 112 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 scripts/README.md create mode 100644 scripts/mcp-probe.mjs diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..33ba257 --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,22 @@ +# scripts + +## mcp-probe.mjs + +Drive the `oab-mcp` core directly over stdio — **without the desktop app** — for +fast credential/region/tool debugging. Same MCP flow the Tauri bridge uses +(`initialize` → `tools/call`), so it exercises the exact AWS credential +resolution the app would, but in a terminal loop with the raw JSON-RPC and the +core's stderr in view. + +```sh +# against the core bundled inside the shipped .app, with your profile: +AWS_PROFILE=brettchien AWS_REGION=ap-east-2 OAB_CLUSTER=oab \ + node scripts/mcp-probe.mjs "/Applications/OAB Studio.app/Contents/MacOS/oab-mcp" + +# a specific tool + args: +node scripts/mcp-probe.mjs ./oab-mcp deploy_get '{"service":"oab-prod-orca"}' +``` + +Whatever `AWS_*` / `AWS_PROFILE` you export is exactly what the app's sidecar +would inherit — so wrong account/region shows up here as an `isError` result or +a stderr line, the same credential-drift symptom, in one shot. diff --git a/scripts/mcp-probe.mjs b/scripts/mcp-probe.mjs new file mode 100644 index 0000000..88e56b1 --- /dev/null +++ b/scripts/mcp-probe.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// mcp-probe — drive the oab-mcp core directly over stdio, without the desktop +// app. Same MCP flow the Tauri bridge uses (initialize → tools/call), so it +// exercises the exact credential/region resolution the app would — but in a +// fast terminal loop with the raw JSON-RPC and the core's stderr in view. +// +// Usage: +// node scripts/mcp-probe.mjs [tool] [json-args] +// +// Examples: +// # against the shipped core inside the .app bundle, with your profile: +// AWS_PROFILE=brettchien AWS_REGION=ap-east-2 OAB_CLUSTER=oab \ +// node scripts/mcp-probe.mjs "/Applications/OAB Studio.app/Contents/MacOS/oab-mcp" +// +// # a specific tool + args: +// node scripts/mcp-probe.mjs ./target/debug/oab-mcp deploy_get '{"service":"oab-prod-orca"}' +// +// The core resolves AWS creds from the standard chain (env → profile → …), so +// whatever AWS_* / AWS_PROFILE you export here is exactly what the app's sidecar +// would inherit. A wrong account/region shows up as an isError result or a +// stderr line — the credential-drift bug, visible in one shot. + +import { spawn } from "node:child_process"; + +const bin = process.argv[2]; +if (!bin) { + console.error("usage: node mcp-probe.mjs [tool] [json-args]"); + process.exit(2); +} +const tool = process.argv[3] || "deploy_list"; +const toolArgs = process.argv[4] ? JSON.parse(process.argv[4]) : {}; +const TIMEOUT_MS = Number(process.env.MCP_PROBE_TIMEOUT_MS || 30000); + +// stderr:inherit → the core's own logs (AWS errors, rmcp) stream straight through. +const child = spawn(bin, [], { stdio: ["pipe", "pipe", "inherit"], env: process.env }); +child.on("error", (e) => { + console.error(`spawn failed: ${e.message}`); + process.exit(1); +}); + +let buf = ""; +let nextId = 1; +const pending = new Map(); + +child.stdout.on("data", (d) => { + buf += d.toString(); + let nl; + while ((nl = buf.indexOf("\n")) >= 0) { + const line = buf.slice(0, nl); + buf = buf.slice(nl + 1); + if (!line.trim()) continue; + let msg; + try { + msg = JSON.parse(line); + } catch { + console.log("« (non-json)", line); + continue; + } + console.log("«", JSON.stringify(msg)); + if (msg.id != null && pending.has(msg.id)) { + pending.get(msg.id)(msg); + pending.delete(msg.id); + } + } +}); + +function send(obj) { + console.log("»", JSON.stringify(obj)); + child.stdin.write(JSON.stringify(obj) + "\n"); +} +function request(method, params) { + const id = nextId++; + return new Promise((res) => { + pending.set(id, res); + send({ jsonrpc: "2.0", id, method, params }); + }); +} + +const timer = setTimeout(() => { + console.error(`\n[timeout after ${TIMEOUT_MS}ms — core did not respond]`); + child.kill("SIGKILL"); + process.exit(1); +}, TIMEOUT_MS); + +try { + await request("initialize", { + protocolVersion: "2024-11-05", + capabilities: {}, + clientInfo: { name: "mcp-probe", version: "0" }, + }); + send({ jsonrpc: "2.0", method: "notifications/initialized" }); + + const resp = await request("tools/call", { name: tool, arguments: toolArgs }); + const result = resp.result ?? {}; + const text = result.content?.[0]?.text; + + console.log(`\n=== ${tool} ===`); + console.log("isError:", result.isError ?? false); + if (text !== undefined) { + try { + console.log(JSON.stringify(JSON.parse(text), null, 2)); + } catch { + console.log(text); + } + } else { + console.log(JSON.stringify(resp, null, 2)); + } +} finally { + clearTimeout(timer); + child.kill("SIGKILL"); +} +process.exit(0);