@mandujs/mcp 0.27.2 → 0.28.1

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 +285 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/mcp",
3
- "version": "0.27.2",
3
+ "version": "0.28.1",
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.1",
38
38
  "@mandujs/ate": "^0.24.0",
39
39
  "@mandujs/skills": "^0.18.0",
40
40
  "@modelcontextprotocol/sdk": "^1.25.3"
@@ -157,6 +157,56 @@ export const brainToolDefinitions: Tool[] = [
157
157
  required: [],
158
158
  },
159
159
  },
160
+ {
161
+ name: "mandu.brain.status",
162
+ description:
163
+ "Check which LLM adapter is active for brain (openai / anthropic / ollama / template) and whether auth tokens are present. Read-only — does not call an LLM or spawn subprocesses.",
164
+ annotations: { readOnlyHint: true },
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {},
168
+ required: [],
169
+ },
170
+ },
171
+ {
172
+ name: "mandu.brain.login",
173
+ description:
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
+ annotations: { readOnlyHint: false },
176
+ inputSchema: {
177
+ type: "object",
178
+ properties: {
179
+ provider: {
180
+ type: "string",
181
+ enum: ["openai", "anthropic"],
182
+ description: "Which provider to sign into. Default: openai.",
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
+ },
189
+ },
190
+ required: [],
191
+ },
192
+ },
193
+ {
194
+ name: "mandu.brain.logout",
195
+ description:
196
+ "Delete stored brain credentials for a provider. For openai, deletes the keychain-stored enterprise token only — the ~/.codex/auth.json owned by the Codex CLI is intentionally left in place (run `npx @openai/codex logout` to revoke that).",
197
+ annotations: { readOnlyHint: false },
198
+ inputSchema: {
199
+ type: "object",
200
+ properties: {
201
+ provider: {
202
+ type: "string",
203
+ enum: ["openai", "anthropic", "all"],
204
+ description: "Which provider to log out of. Default: all.",
205
+ },
206
+ },
207
+ required: [],
208
+ },
209
+ },
160
210
  ];
161
211
 
162
212
  /** Module-level unsubscribe handle for MCP warning notifications */
@@ -547,6 +597,241 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
547
597
  },
548
598
  };
549
599
 
600
+ // #235 followup — brain auth tools (status / login / logout).
601
+ handlers["mandu.brain.status"] = async () => {
602
+ const core = await import("@mandujs/core");
603
+ const store = core.getCredentialStore();
604
+ const resolution = await core.resolveBrainAdapter({
605
+ adapter: "auto",
606
+ credentialStore: store,
607
+ projectRoot,
608
+ });
609
+
610
+ // Check ChatGPT session token (managed by @openai/codex, not the keychain).
611
+ const chatgpt = new core.ChatGPTAuth();
612
+ const chatgptFile = chatgpt.locateAuthFile();
613
+
614
+ const providers: Record<string, unknown> = {};
615
+ for (const provider of ["openai", "anthropic"] as const) {
616
+ const token = await store.load(provider);
617
+ providers[provider] = token
618
+ ? {
619
+ logged_in: true,
620
+ source: "keychain",
621
+ model: token.default_model ?? null,
622
+ expires_at: token.expires_at
623
+ ? new Date(token.expires_at * 1000).toISOString()
624
+ : null,
625
+ last_used_at: token.last_used_at ?? null,
626
+ }
627
+ : provider === "openai" && chatgptFile
628
+ ? {
629
+ logged_in: true,
630
+ source: "chatgpt_session",
631
+ auth_file: chatgptFile,
632
+ note: "Managed by `@openai/codex` CLI. Mandu reads + auto-refreshes.",
633
+ }
634
+ : { logged_in: false };
635
+ }
636
+
637
+ return {
638
+ content: [
639
+ {
640
+ type: "text",
641
+ text: JSON.stringify(
642
+ {
643
+ active_tier: resolution.resolved,
644
+ reason: resolution.reason,
645
+ backend: store.backendName,
646
+ providers,
647
+ },
648
+ null,
649
+ 2,
650
+ ),
651
+ },
652
+ ],
653
+ };
654
+ };
655
+
656
+ handlers["mandu.brain.login"] = async (args) => {
657
+ const { provider = "openai", waitMs = 180000 } = args as {
658
+ provider?: "openai" | "anthropic";
659
+ waitMs?: number;
660
+ };
661
+
662
+ if (provider === "openai") {
663
+ const core = await import("@mandujs/core");
664
+ const auth = new core.ChatGPTAuth();
665
+ const existing = auth.locateAuthFile();
666
+ if (existing) {
667
+ return {
668
+ content: [
669
+ {
670
+ type: "text",
671
+ text: JSON.stringify(
672
+ {
673
+ ok: true,
674
+ provider: "openai",
675
+ already_authenticated: true,
676
+ auth_file: existing,
677
+ note:
678
+ "ChatGPT session already present. Call mandu.brain.logout + mandu.brain.login again to re-authenticate.",
679
+ },
680
+ null,
681
+ 2,
682
+ ),
683
+ },
684
+ ],
685
+ };
686
+ }
687
+
688
+ // Spawn `npx @openai/codex login` detached from the MCP process.
689
+ // Codex itself opens the user's default browser (`start` on
690
+ // Windows, `open` on macOS, `xdg-open` on Linux) — no TTY needed
691
+ // on our side. We poll for ~/.codex/auth.json to appear and
692
+ // return once it does.
693
+ const { spawn } = await import("node:child_process");
694
+ const child = spawn("npx", ["-y", "@openai/codex", "login"], {
695
+ cwd: projectRoot,
696
+ detached: false,
697
+ stdio: ["ignore", "pipe", "pipe"],
698
+ shell: process.platform === "win32",
699
+ });
700
+
701
+ let stdoutBuffer = "";
702
+ let stderrBuffer = "";
703
+ child.stdout?.on("data", (d) => {
704
+ stdoutBuffer += d.toString();
705
+ });
706
+ child.stderr?.on("data", (d) => {
707
+ stderrBuffer += d.toString();
708
+ });
709
+
710
+ const deadline = Date.now() + Math.max(15_000, Math.min(waitMs, 600_000));
711
+ let file: string | null = null;
712
+ while (Date.now() < deadline) {
713
+ file = auth.locateAuthFile();
714
+ if (file) break;
715
+ await new Promise((r) => setTimeout(r, 1000));
716
+ }
717
+
718
+ // Kill the codex process if it's still running (normally it exits
719
+ // on its own once auth.json is written).
720
+ if (!child.killed) {
721
+ try { child.kill(); } catch { /* ignore */ }
722
+ }
723
+
724
+ const urlMatch = stdoutBuffer.match(
725
+ /https:\/\/auth\.openai\.com\/oauth\/authorize\?[^\s]+/,
726
+ );
727
+
728
+ return {
729
+ content: [
730
+ {
731
+ type: "text",
732
+ text: JSON.stringify(
733
+ {
734
+ ok: Boolean(file),
735
+ provider: "openai",
736
+ auth_file: file,
737
+ oauth_url: urlMatch ? urlMatch[0] : undefined,
738
+ stdout_tail: stdoutBuffer.slice(-500),
739
+ stderr_tail: stderrBuffer.slice(-500),
740
+ note: file
741
+ ? "auth.json written; Mandu brain will now use the OpenAI tier."
742
+ : "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.",
743
+ },
744
+ null,
745
+ 2,
746
+ ),
747
+ },
748
+ ],
749
+ };
750
+ }
751
+
752
+ // Anthropic — Mandu-managed OAuth loopback flow.
753
+ const core = await import("@mandujs/core");
754
+ try {
755
+ const adapter = new core.AnthropicOAuthAdapter({
756
+ credentialStore: core.getCredentialStore(),
757
+ projectRoot,
758
+ strict: true,
759
+ skipConsent: true,
760
+ });
761
+ const token = await adapter.login({});
762
+ return {
763
+ content: [
764
+ {
765
+ type: "text",
766
+ text: JSON.stringify(
767
+ {
768
+ ok: true,
769
+ provider: "anthropic",
770
+ model: token.default_model ?? null,
771
+ expires_at: token.expires_at
772
+ ? new Date(token.expires_at * 1000).toISOString()
773
+ : null,
774
+ },
775
+ null,
776
+ 2,
777
+ ),
778
+ },
779
+ ],
780
+ };
781
+ } catch (err) {
782
+ return {
783
+ content: [
784
+ {
785
+ type: "text",
786
+ text: JSON.stringify(
787
+ {
788
+ ok: false,
789
+ provider: "anthropic",
790
+ error: err instanceof Error ? err.message : String(err),
791
+ },
792
+ null,
793
+ 2,
794
+ ),
795
+ },
796
+ ],
797
+ };
798
+ }
799
+ };
800
+
801
+ handlers["mandu.brain.logout"] = async (args) => {
802
+ const { provider = "all" } = args as {
803
+ provider?: "openai" | "anthropic" | "all";
804
+ };
805
+ const core = await import("@mandujs/core");
806
+ const store = core.getCredentialStore();
807
+ const targets =
808
+ provider === "all"
809
+ ? (["openai", "anthropic"] as const)
810
+ : ([provider] as const);
811
+ for (const p of targets) {
812
+ await store.delete(p);
813
+ await core.revokeConsent(p, projectRoot);
814
+ }
815
+ return {
816
+ content: [
817
+ {
818
+ type: "text",
819
+ text: JSON.stringify(
820
+ {
821
+ ok: true,
822
+ logged_out: targets,
823
+ note: targets.includes("openai")
824
+ ? "Keychain-stored openai token cleared. To revoke the Codex CLI session (~/.codex/auth.json), run `npx @openai/codex logout`."
825
+ : undefined,
826
+ },
827
+ null,
828
+ 2,
829
+ ),
830
+ },
831
+ ],
832
+ };
833
+ };
834
+
550
835
  // Backward-compatible aliases (deprecated)
551
836
  handlers["mandu_doctor"] = handlers["mandu.brain.doctor"];
552
837
  handlers["mandu_watch_start"] = handlers["mandu.watch.start"];