@mandujs/mcp 0.28.0 → 0.28.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/tools/brain.ts +104 -23
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/mcp",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
4
4
  "description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@mandujs/core": "^0.41.0",
37
+ "@mandujs/core": "^0.41.2",
38
38
  "@mandujs/ate": "^0.24.0",
39
39
  "@mandujs/skills": "^0.18.0",
40
40
  "@modelcontextprotocol/sdk": "^1.25.3"
@@ -171,7 +171,7 @@ export const brainToolDefinitions: Tool[] = [
171
171
  {
172
172
  name: "mandu.brain.login",
173
173
  description:
174
- "Authenticate the brain to an LLM provider. For openai, delegates to the OpenAI-official `@openai/codex` CLI (writes ~/.codex/auth.json; Mandu reads + auto-refreshes). MUST be invoked from a context where a terminal / browser is available — the CLI opens a browser tab for OAuth. Anthropic path uses the Mandu OAuth flow with a local loopback listener.",
174
+ "Authenticate the brain to an LLM provider. For openai, spawns `npx @openai/codex login` which opens the user's default browser to the OpenAI OAuth page; on approval, the token lands in ~/.codex/auth.json and this tool returns. Anthropic path uses the Mandu OAuth flow with a local loopback listener.",
175
175
  annotations: { readOnlyHint: false },
176
176
  inputSchema: {
177
177
  type: "object",
@@ -181,6 +181,11 @@ export const brainToolDefinitions: Tool[] = [
181
181
  enum: ["openai", "anthropic"],
182
182
  description: "Which provider to sign into. Default: openai.",
183
183
  },
184
+ waitMs: {
185
+ type: "number",
186
+ description:
187
+ "How long to wait for auth.json to appear after spawning the OAuth flow. Default 180000 (3 min). Increase if the user takes longer to approve in the browser.",
188
+ },
184
189
  },
185
190
  required: [],
186
191
  },
@@ -207,6 +212,40 @@ export const brainToolDefinitions: Tool[] = [
207
212
  /** Module-level unsubscribe handle for MCP warning notifications */
208
213
  let mcpWarningUnsubscribe: (() => void) | null = null;
209
214
 
215
+ /**
216
+ * #236 — surface a clear error when a stale `@mandujs/core` resolves
217
+ * under `node_modules/@mandujs/mcp/node_modules/` (Bun's installer
218
+ * sometimes lands an older nested copy even with `linker=hoisted`).
219
+ * Without this check the user saw `getCredentialStore is not a
220
+ * function` / `undefined is not a constructor` with no hint.
221
+ */
222
+ function assertBrainAuthSurface(core: Record<string, unknown>): void {
223
+ const missing: string[] = [];
224
+ if (typeof core.getCredentialStore !== "function")
225
+ missing.push("getCredentialStore");
226
+ if (typeof core.resolveBrainAdapter !== "function")
227
+ missing.push("resolveBrainAdapter");
228
+ if (typeof core.ChatGPTAuth !== "function") missing.push("ChatGPTAuth");
229
+ if (typeof core.AnthropicOAuthAdapter !== "function")
230
+ missing.push("AnthropicOAuthAdapter");
231
+ if (typeof core.revokeConsent !== "function") missing.push("revokeConsent");
232
+ if (missing.length === 0) return;
233
+
234
+ const pkgVersion =
235
+ typeof core.__MANDU_CORE_VERSION__ === "string"
236
+ ? core.__MANDU_CORE_VERSION__
237
+ : "unknown";
238
+ throw new Error(
239
+ `[mandu-mcp] The resolved @mandujs/core (v${pkgVersion}) is missing brain-auth exports: ${missing.join(
240
+ ", ",
241
+ )}. ` +
242
+ `This usually means Bun's installer placed a stale nested copy at ` +
243
+ `node_modules/@mandujs/mcp/node_modules/@mandujs/core instead of hoisting to the top level. ` +
244
+ `Fix: \`rm -rf node_modules bun.lock && bun install\` (or confirm linker=hoisted in bunfig.toml). ` +
245
+ `See https://github.com/konamgil/mandu/issues/236 for details.`,
246
+ );
247
+ }
248
+
210
249
  export function brainTools(projectRoot: string, server?: Server, monitor?: ActivityMonitor) {
211
250
  const paths = getProjectPaths(projectRoot);
212
251
 
@@ -595,6 +634,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
595
634
  // #235 followup — brain auth tools (status / login / logout).
596
635
  handlers["mandu.brain.status"] = async () => {
597
636
  const core = await import("@mandujs/core");
637
+ assertBrainAuthSurface(core);
598
638
  const store = core.getCredentialStore();
599
639
  const resolution = await core.resolveBrainAdapter({
600
640
  adapter: "auto",
@@ -649,26 +689,29 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
649
689
  };
650
690
 
651
691
  handlers["mandu.brain.login"] = async (args) => {
652
- const { provider = "openai" } = args as { provider?: "openai" | "anthropic" };
653
- const { spawnSync } = await import("node:child_process");
692
+ const { provider = "openai", waitMs = 180000 } = args as {
693
+ provider?: "openai" | "anthropic";
694
+ waitMs?: number;
695
+ };
654
696
 
655
697
  if (provider === "openai") {
656
- // Delegate to the OpenAI-official Codex CLI. This MUST run
657
- // interactively (browser-based OAuth). If stdin/stdout aren't a
658
- // TTY the agent should surface the command for the user to run
659
- // manually instead of attempting to spawn.
660
- const isTty = Boolean(process.stdout.isTTY && process.stdin.isTTY);
661
- if (!isTty) {
698
+ const core = await import("@mandujs/core");
699
+ assertBrainAuthSurface(core);
700
+ const auth = new core.ChatGPTAuth();
701
+ const existing = auth.locateAuthFile();
702
+ if (existing) {
662
703
  return {
663
704
  content: [
664
705
  {
665
706
  type: "text",
666
707
  text: JSON.stringify(
667
708
  {
668
- ok: false,
669
- reason: "not_a_tty",
670
- instruction:
671
- "Run this in your terminal, then call mandu.brain.status:\n\n npx @openai/codex login\n",
709
+ ok: true,
710
+ provider: "openai",
711
+ already_authenticated: true,
712
+ auth_file: existing,
713
+ note:
714
+ "ChatGPT session already present. Call mandu.brain.logout + mandu.brain.login again to re-authenticate.",
672
715
  },
673
716
  null,
674
717
  2,
@@ -677,26 +720,62 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
677
720
  ],
678
721
  };
679
722
  }
680
- const result = spawnSync("npx", ["-y", "@openai/codex", "login"], {
681
- stdio: "inherit",
723
+
724
+ // Spawn `npx @openai/codex login` detached from the MCP process.
725
+ // Codex itself opens the user's default browser (`start` on
726
+ // Windows, `open` on macOS, `xdg-open` on Linux) — no TTY needed
727
+ // on our side. We poll for ~/.codex/auth.json to appear and
728
+ // return once it does.
729
+ const { spawn } = await import("node:child_process");
730
+ const child = spawn("npx", ["-y", "@openai/codex", "login"], {
731
+ cwd: projectRoot,
732
+ detached: false,
733
+ stdio: ["ignore", "pipe", "pipe"],
682
734
  shell: process.platform === "win32",
683
735
  });
684
- const core = await import("@mandujs/core");
685
- const auth = new core.ChatGPTAuth();
686
- const file = auth.locateAuthFile();
736
+
737
+ let stdoutBuffer = "";
738
+ let stderrBuffer = "";
739
+ child.stdout?.on("data", (d) => {
740
+ stdoutBuffer += d.toString();
741
+ });
742
+ child.stderr?.on("data", (d) => {
743
+ stderrBuffer += d.toString();
744
+ });
745
+
746
+ const deadline = Date.now() + Math.max(15_000, Math.min(waitMs, 600_000));
747
+ let file: string | null = null;
748
+ while (Date.now() < deadline) {
749
+ file = auth.locateAuthFile();
750
+ if (file) break;
751
+ await new Promise((r) => setTimeout(r, 1000));
752
+ }
753
+
754
+ // Kill the codex process if it's still running (normally it exits
755
+ // on its own once auth.json is written).
756
+ if (!child.killed) {
757
+ try { child.kill(); } catch { /* ignore */ }
758
+ }
759
+
760
+ const urlMatch = stdoutBuffer.match(
761
+ /https:\/\/auth\.openai\.com\/oauth\/authorize\?[^\s]+/,
762
+ );
763
+
687
764
  return {
688
765
  content: [
689
766
  {
690
767
  type: "text",
691
768
  text: JSON.stringify(
692
769
  {
693
- ok: result.status === 0 && Boolean(file),
694
- exit_code: result.status,
695
- auth_file: file,
770
+ ok: Boolean(file),
696
771
  provider: "openai",
772
+ auth_file: file,
773
+ oauth_url: urlMatch ? urlMatch[0] : undefined,
774
+ stdout_tail: stdoutBuffer.slice(-500),
775
+ stderr_tail: stderrBuffer.slice(-500),
697
776
  note: file
698
- ? "auth.json present; brain will use it automatically."
699
- : "Login flow exited without writing auth.json.",
777
+ ? "auth.json written; Mandu brain will now use the OpenAI tier."
778
+ : "No auth.json detected before waitMs expired. If the OAuth URL is present above, open it in a browser; otherwise rerun with a larger waitMs or run `npx @openai/codex login` in your own terminal.",
700
779
  },
701
780
  null,
702
781
  2,
@@ -708,6 +787,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
708
787
 
709
788
  // Anthropic — Mandu-managed OAuth loopback flow.
710
789
  const core = await import("@mandujs/core");
790
+ assertBrainAuthSurface(core);
711
791
  try {
712
792
  const adapter = new core.AnthropicOAuthAdapter({
713
793
  credentialStore: core.getCredentialStore(),
@@ -760,6 +840,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
760
840
  provider?: "openai" | "anthropic" | "all";
761
841
  };
762
842
  const core = await import("@mandujs/core");
843
+ assertBrainAuthSurface(core);
763
844
  const store = core.getCredentialStore();
764
845
  const targets =
765
846
  provider === "all"