codemaxxing 0.3.0 → 0.3.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.
- package/dist/agent.d.ts +7 -0
- package/dist/agent.js +51 -2
- package/dist/exec.js +10 -0
- package/dist/index.js +85 -0
- package/dist/utils/mcp.d.ts +55 -0
- package/dist/utils/mcp.js +251 -0
- package/package.json +2 -1
- package/src/agent.ts +55 -2
- package/src/exec.ts +12 -0
- package/src/index.tsx +86 -0
- package/src/utils/mcp.ts +307 -0
package/dist/agent.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ConnectedServer } from "./utils/mcp.js";
|
|
1
2
|
import type { ProviderConfig } from "./config.js";
|
|
2
3
|
export interface AgentOptions {
|
|
3
4
|
provider: ProviderConfig;
|
|
@@ -13,6 +14,7 @@ export interface AgentOptions {
|
|
|
13
14
|
onContextCompressed?: (oldTokens: number, newTokens: number) => void;
|
|
14
15
|
onArchitectPlan?: (plan: string) => void;
|
|
15
16
|
onLintResult?: (file: string, errors: string) => void;
|
|
17
|
+
onMCPStatus?: (server: string, status: string) => void;
|
|
16
18
|
contextCompressionThreshold?: number;
|
|
17
19
|
}
|
|
18
20
|
export declare class CodingAgent {
|
|
@@ -41,6 +43,7 @@ export declare class CodingAgent {
|
|
|
41
43
|
private architectModel;
|
|
42
44
|
private autoLintEnabled;
|
|
43
45
|
private detectedLinter;
|
|
46
|
+
private mcpServers;
|
|
44
47
|
constructor(options: AgentOptions);
|
|
45
48
|
/**
|
|
46
49
|
* Initialize the agent — call this after constructor to build async context
|
|
@@ -124,5 +127,9 @@ export declare class CodingAgent {
|
|
|
124
127
|
* Run the architect model to generate a plan, then feed to editor model
|
|
125
128
|
*/
|
|
126
129
|
private architectChat;
|
|
130
|
+
getMCPServerCount(): number;
|
|
131
|
+
getMCPServers(): ConnectedServer[];
|
|
132
|
+
disconnectMCP(): Promise<void>;
|
|
133
|
+
reconnectMCP(): Promise<void>;
|
|
127
134
|
reset(): void;
|
|
128
135
|
}
|
package/dist/agent.js
CHANGED
|
@@ -6,6 +6,7 @@ import { buildProjectContext, getSystemPrompt, loadProjectRules } from "./utils/
|
|
|
6
6
|
import { isGitRepo, autoCommit } from "./utils/git.js";
|
|
7
7
|
import { buildSkillPrompts, getActiveSkillCount } from "./utils/skills.js";
|
|
8
8
|
import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
|
|
9
|
+
import { loadMCPConfig, connectToServers, disconnectAll, getAllMCPTools, parseMCPToolName, callMCPTool } from "./utils/mcp.js";
|
|
9
10
|
// Tools that can modify your project — require approval
|
|
10
11
|
const DANGEROUS_TOOLS = new Set(["write_file", "run_command"]);
|
|
11
12
|
// Cost per 1M tokens (input/output) for common models
|
|
@@ -78,6 +79,7 @@ export class CodingAgent {
|
|
|
78
79
|
architectModel = null;
|
|
79
80
|
autoLintEnabled = true;
|
|
80
81
|
detectedLinter = null;
|
|
82
|
+
mcpServers = [];
|
|
81
83
|
constructor(options) {
|
|
82
84
|
this.options = options;
|
|
83
85
|
this.providerType = options.provider.type || "openai";
|
|
@@ -113,6 +115,15 @@ export class CodingAgent {
|
|
|
113
115
|
this.systemPrompt = await getSystemPrompt(context, skillPrompts, rules?.content ?? "");
|
|
114
116
|
// Detect project linter
|
|
115
117
|
this.detectedLinter = detectLinter(this.cwd);
|
|
118
|
+
// Connect to MCP servers
|
|
119
|
+
const mcpConfig = loadMCPConfig(this.cwd);
|
|
120
|
+
if (Object.keys(mcpConfig.mcpServers).length > 0) {
|
|
121
|
+
this.mcpServers = await connectToServers(mcpConfig, this.options.onMCPStatus);
|
|
122
|
+
if (this.mcpServers.length > 0) {
|
|
123
|
+
const mcpTools = getAllMCPTools(this.mcpServers);
|
|
124
|
+
this.tools = [...FILE_TOOLS, ...mcpTools];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
116
127
|
this.messages = [
|
|
117
128
|
{ role: "system", content: this.systemPrompt },
|
|
118
129
|
];
|
|
@@ -305,7 +316,15 @@ export class CodingAgent {
|
|
|
305
316
|
}
|
|
306
317
|
}
|
|
307
318
|
}
|
|
308
|
-
|
|
319
|
+
// Route to MCP or built-in tool
|
|
320
|
+
const mcpParsed = parseMCPToolName(toolCall.name);
|
|
321
|
+
let result;
|
|
322
|
+
if (mcpParsed) {
|
|
323
|
+
result = await callMCPTool(mcpParsed.serverName, mcpParsed.toolName, args);
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
result = await executeTool(toolCall.name, args, this.cwd);
|
|
327
|
+
}
|
|
309
328
|
this.options.onToolResult?.(toolCall.name, result);
|
|
310
329
|
// Auto-commit after successful write_file (only if enabled)
|
|
311
330
|
if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
|
|
@@ -502,7 +521,15 @@ export class CodingAgent {
|
|
|
502
521
|
}
|
|
503
522
|
}
|
|
504
523
|
}
|
|
505
|
-
|
|
524
|
+
// Route to MCP or built-in tool
|
|
525
|
+
const mcpParsed = parseMCPToolName(toolCall.name);
|
|
526
|
+
let result;
|
|
527
|
+
if (mcpParsed) {
|
|
528
|
+
result = await callMCPTool(mcpParsed.serverName, mcpParsed.toolName, args);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
result = await executeTool(toolCall.name, args, this.cwd);
|
|
532
|
+
}
|
|
506
533
|
this.options.onToolResult?.(toolCall.name, result);
|
|
507
534
|
// Auto-commit after successful write_file
|
|
508
535
|
if (this.gitEnabled && this.autoCommitEnabled && toolCall.name === "write_file" && result.startsWith("✅")) {
|
|
@@ -738,6 +765,28 @@ export class CodingAgent {
|
|
|
738
765
|
const editorPrompt = `## Architect Plan\n${plan}\n\n## Original Request\n${userMessage}\n\nExecute the plan above. Follow it step by step.`;
|
|
739
766
|
return this.chat(editorPrompt);
|
|
740
767
|
}
|
|
768
|
+
getMCPServerCount() {
|
|
769
|
+
return this.mcpServers.length;
|
|
770
|
+
}
|
|
771
|
+
getMCPServers() {
|
|
772
|
+
return this.mcpServers;
|
|
773
|
+
}
|
|
774
|
+
async disconnectMCP() {
|
|
775
|
+
await disconnectAll();
|
|
776
|
+
this.mcpServers = [];
|
|
777
|
+
this.tools = FILE_TOOLS;
|
|
778
|
+
}
|
|
779
|
+
async reconnectMCP() {
|
|
780
|
+
await this.disconnectMCP();
|
|
781
|
+
const mcpConfig = loadMCPConfig(this.cwd);
|
|
782
|
+
if (Object.keys(mcpConfig.mcpServers).length > 0) {
|
|
783
|
+
this.mcpServers = await connectToServers(mcpConfig, this.options.onMCPStatus);
|
|
784
|
+
if (this.mcpServers.length > 0) {
|
|
785
|
+
const mcpTools = getAllMCPTools(this.mcpServers);
|
|
786
|
+
this.tools = [...FILE_TOOLS, ...mcpTools];
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
741
790
|
reset() {
|
|
742
791
|
const systemMsg = this.messages[0];
|
|
743
792
|
this.messages = [systemMsg];
|
package/dist/exec.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { CodingAgent } from "./agent.js";
|
|
8
8
|
import { loadConfig, applyOverrides, detectLocalProvider } from "./config.js";
|
|
9
|
+
import { disconnectAll } from "./utils/mcp.js";
|
|
9
10
|
function parseExecArgs(argv) {
|
|
10
11
|
const args = {
|
|
11
12
|
prompt: "",
|
|
@@ -124,9 +125,16 @@ export async function runExec(argv) {
|
|
|
124
125
|
process.stderr.write(`⚠ Denied ${name} (use --auto-approve to allow)\n`);
|
|
125
126
|
return "no";
|
|
126
127
|
},
|
|
128
|
+
onMCPStatus: (server, status) => {
|
|
129
|
+
process.stderr.write(`MCP ${server}: ${status}\n`);
|
|
130
|
+
},
|
|
127
131
|
});
|
|
128
132
|
try {
|
|
129
133
|
await agent.init();
|
|
134
|
+
const mcpCount = agent.getMCPServerCount();
|
|
135
|
+
if (mcpCount > 0) {
|
|
136
|
+
process.stderr.write(`MCP: ${mcpCount} server${mcpCount > 1 ? "s" : ""} connected\n`);
|
|
137
|
+
}
|
|
130
138
|
await agent.send(args.prompt);
|
|
131
139
|
if (!args.json) {
|
|
132
140
|
// Ensure newline at end of output
|
|
@@ -142,9 +150,11 @@ export async function runExec(argv) {
|
|
|
142
150
|
};
|
|
143
151
|
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
144
152
|
}
|
|
153
|
+
await disconnectAll();
|
|
145
154
|
process.exit(hasChanges ? 0 : 2);
|
|
146
155
|
}
|
|
147
156
|
catch (err) {
|
|
157
|
+
await disconnectAll();
|
|
148
158
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
149
159
|
if (args.json) {
|
|
150
160
|
process.stdout.write(JSON.stringify({ error: err.message }, null, 2) + "\n");
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./util
|
|
|
12
12
|
import { getTheme, listThemes, THEMES, DEFAULT_THEME } from "./themes.js";
|
|
13
13
|
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow } from "./utils/auth.js";
|
|
14
14
|
import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
|
|
15
|
+
import { listServers, addServer, removeServer, getConnectedServers } from "./utils/mcp.js";
|
|
15
16
|
const VERSION = "0.1.9";
|
|
16
17
|
// ── Helpers ──
|
|
17
18
|
function formatTimeAgo(date) {
|
|
@@ -58,6 +59,11 @@ const SLASH_COMMANDS = [
|
|
|
58
59
|
{ cmd: "/lint", desc: "show auto-lint status" },
|
|
59
60
|
{ cmd: "/lint on", desc: "enable auto-lint" },
|
|
60
61
|
{ cmd: "/lint off", desc: "disable auto-lint" },
|
|
62
|
+
{ cmd: "/mcp", desc: "show MCP servers" },
|
|
63
|
+
{ cmd: "/mcp tools", desc: "list MCP tools" },
|
|
64
|
+
{ cmd: "/mcp add", desc: "add MCP server" },
|
|
65
|
+
{ cmd: "/mcp remove", desc: "remove MCP server" },
|
|
66
|
+
{ cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
|
|
61
67
|
{ cmd: "/quit", desc: "exit" },
|
|
62
68
|
];
|
|
63
69
|
const SPINNER_FRAMES = ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"];
|
|
@@ -246,6 +252,9 @@ function App() {
|
|
|
246
252
|
onLintResult: (file, errors) => {
|
|
247
253
|
addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
|
|
248
254
|
},
|
|
255
|
+
onMCPStatus: (server, status) => {
|
|
256
|
+
addMsg("info", `🔌 MCP ${server}: ${status}`);
|
|
257
|
+
},
|
|
249
258
|
contextCompressionThreshold: config.defaults.contextCompressionThreshold,
|
|
250
259
|
onToolApproval: (name, args, diff) => {
|
|
251
260
|
return new Promise((resolve) => {
|
|
@@ -262,6 +271,12 @@ function App() {
|
|
|
262
271
|
info.push(`📋 ${rulesSource} loaded`);
|
|
263
272
|
setConnectionInfo([...info]);
|
|
264
273
|
}
|
|
274
|
+
// Show MCP server count
|
|
275
|
+
const mcpCount = a.getMCPServerCount();
|
|
276
|
+
if (mcpCount > 0) {
|
|
277
|
+
info.push(`🔌 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} connected`);
|
|
278
|
+
setConnectionInfo([...info]);
|
|
279
|
+
}
|
|
265
280
|
setAgent(a);
|
|
266
281
|
setModelName(provider.model);
|
|
267
282
|
providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
|
|
@@ -367,6 +382,11 @@ function App() {
|
|
|
367
382
|
" /lint — show auto-lint status & detected linter",
|
|
368
383
|
" /lint on — enable auto-lint",
|
|
369
384
|
" /lint off — disable auto-lint",
|
|
385
|
+
" /mcp — show MCP servers & status",
|
|
386
|
+
" /mcp tools — list all MCP tools",
|
|
387
|
+
" /mcp add — add MCP server to global config",
|
|
388
|
+
" /mcp remove — remove MCP server",
|
|
389
|
+
" /mcp reconnect — reconnect all MCP servers",
|
|
370
390
|
" /quit — exit",
|
|
371
391
|
].join("\n"));
|
|
372
392
|
return;
|
|
@@ -534,6 +554,71 @@ function App() {
|
|
|
534
554
|
addMsg("info", "🔍 Auto-lint OFF");
|
|
535
555
|
return;
|
|
536
556
|
}
|
|
557
|
+
// ── MCP commands (partially work without agent) ──
|
|
558
|
+
if (trimmed === "/mcp" || trimmed === "/mcp list") {
|
|
559
|
+
const servers = listServers(process.cwd());
|
|
560
|
+
if (servers.length === 0) {
|
|
561
|
+
addMsg("info", "🔌 No MCP servers configured.\n Add one: /mcp add <name> <command> [args...]");
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
const lines = servers.map((s) => {
|
|
565
|
+
const status = s.connected ? `✔ connected (${s.toolCount} tools)` : "✗ not connected";
|
|
566
|
+
return ` ${s.connected ? "●" : "○"} ${s.name} [${s.source}] — ${s.command}\n ${status}`;
|
|
567
|
+
});
|
|
568
|
+
addMsg("info", `🔌 MCP Servers:\n${lines.join("\n")}`);
|
|
569
|
+
}
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (trimmed === "/mcp tools") {
|
|
573
|
+
const servers = getConnectedServers();
|
|
574
|
+
if (servers.length === 0) {
|
|
575
|
+
addMsg("info", "🔌 No MCP servers connected.");
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
const lines = [];
|
|
579
|
+
for (const server of servers) {
|
|
580
|
+
lines.push(`${server.name} (${server.tools.length} tools):`);
|
|
581
|
+
for (const tool of server.tools) {
|
|
582
|
+
lines.push(` • ${tool.name} — ${tool.description ?? "(no description)"}`);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
addMsg("info", `🔌 MCP Tools:\n${lines.join("\n")}`);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (trimmed.startsWith("/mcp add ")) {
|
|
589
|
+
const parts = trimmed.replace("/mcp add ", "").trim().split(/\s+/);
|
|
590
|
+
if (parts.length < 2) {
|
|
591
|
+
addMsg("info", "Usage: /mcp add <name> <command> [args...]\n Example: /mcp add github npx -y @modelcontextprotocol/server-github");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const [name, command, ...cmdArgs] = parts;
|
|
595
|
+
const result = addServer(name, { command, args: cmdArgs.length > 0 ? cmdArgs : undefined });
|
|
596
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (trimmed.startsWith("/mcp remove ")) {
|
|
600
|
+
const name = trimmed.replace("/mcp remove ", "").trim();
|
|
601
|
+
if (!name) {
|
|
602
|
+
addMsg("info", "Usage: /mcp remove <name>");
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const result = removeServer(name);
|
|
606
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (trimmed === "/mcp reconnect") {
|
|
610
|
+
if (!agent) {
|
|
611
|
+
addMsg("info", "⚠ No agent connected. Connect first.");
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
addMsg("info", "🔌 Reconnecting MCP servers...");
|
|
615
|
+
await agent.reconnectMCP();
|
|
616
|
+
const count = agent.getMCPServerCount();
|
|
617
|
+
addMsg("info", count > 0
|
|
618
|
+
? `✅ ${count} MCP server${count > 1 ? "s" : ""} reconnected.`
|
|
619
|
+
: "No MCP servers connected.");
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
537
622
|
// Commands below require an active LLM connection
|
|
538
623
|
if (!agent) {
|
|
539
624
|
addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) client support
|
|
3
|
+
* Connects to external MCP servers and exposes their tools to the LLM agent.
|
|
4
|
+
*/
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
import type { ChatCompletionTool } from "openai/resources/chat/completions";
|
|
8
|
+
export interface MCPServerConfig {
|
|
9
|
+
command: string;
|
|
10
|
+
args?: string[];
|
|
11
|
+
env?: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
export interface MCPConfig {
|
|
14
|
+
mcpServers: Record<string, MCPServerConfig>;
|
|
15
|
+
}
|
|
16
|
+
export interface ConnectedServer {
|
|
17
|
+
name: string;
|
|
18
|
+
client: Client;
|
|
19
|
+
transport: StdioClientTransport;
|
|
20
|
+
tools: Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
inputSchema: Record<string, unknown>;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export declare function loadMCPConfig(cwd: string): MCPConfig;
|
|
27
|
+
export declare function connectToServers(config: MCPConfig, onStatus?: (name: string, status: string) => void): Promise<ConnectedServer[]>;
|
|
28
|
+
export declare function disconnectAll(): Promise<void>;
|
|
29
|
+
export declare function getConnectedServers(): ConnectedServer[];
|
|
30
|
+
export declare function getAllMCPTools(servers: ConnectedServer[]): ChatCompletionTool[];
|
|
31
|
+
/**
|
|
32
|
+
* Parse an MCP tool call name to extract server name and tool name.
|
|
33
|
+
* Format: mcp_<serverName>_<toolName>
|
|
34
|
+
* Server names can contain hyphens but not underscores (by convention).
|
|
35
|
+
*/
|
|
36
|
+
export declare function parseMCPToolName(fullName: string): {
|
|
37
|
+
serverName: string;
|
|
38
|
+
toolName: string;
|
|
39
|
+
} | null;
|
|
40
|
+
export declare function callMCPTool(serverName: string, toolName: string, args: Record<string, unknown>): Promise<string>;
|
|
41
|
+
export declare function addServer(name: string, config: MCPServerConfig): {
|
|
42
|
+
ok: boolean;
|
|
43
|
+
message: string;
|
|
44
|
+
};
|
|
45
|
+
export declare function removeServer(name: string): {
|
|
46
|
+
ok: boolean;
|
|
47
|
+
message: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function listServers(cwd: string): Array<{
|
|
50
|
+
name: string;
|
|
51
|
+
source: string;
|
|
52
|
+
command: string;
|
|
53
|
+
connected: boolean;
|
|
54
|
+
toolCount: number;
|
|
55
|
+
}>;
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) client support
|
|
3
|
+
* Connects to external MCP servers and exposes their tools to the LLM agent.
|
|
4
|
+
*/
|
|
5
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
6
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
7
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
// ── Config paths ──
|
|
11
|
+
const GLOBAL_CONFIG_DIR = join(homedir(), ".codemaxxing");
|
|
12
|
+
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, "mcp.json");
|
|
13
|
+
function getProjectConfigPaths(cwd) {
|
|
14
|
+
return [
|
|
15
|
+
join(cwd, ".codemaxxing", "mcp.json"),
|
|
16
|
+
join(cwd, ".cursor", "mcp.json"),
|
|
17
|
+
join(cwd, "opencode.json"),
|
|
18
|
+
];
|
|
19
|
+
}
|
|
20
|
+
// ── Config loading ──
|
|
21
|
+
function loadConfigFile(path) {
|
|
22
|
+
try {
|
|
23
|
+
if (!existsSync(path))
|
|
24
|
+
return null;
|
|
25
|
+
const raw = readFileSync(path, "utf-8");
|
|
26
|
+
const parsed = JSON.parse(raw);
|
|
27
|
+
if (parsed.mcpServers && typeof parsed.mcpServers === "object") {
|
|
28
|
+
return parsed;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function loadMCPConfig(cwd) {
|
|
37
|
+
const merged = { mcpServers: {} };
|
|
38
|
+
// Load global config first (lower priority)
|
|
39
|
+
const globalConfig = loadConfigFile(GLOBAL_CONFIG_PATH);
|
|
40
|
+
if (globalConfig) {
|
|
41
|
+
Object.assign(merged.mcpServers, globalConfig.mcpServers);
|
|
42
|
+
}
|
|
43
|
+
// Load project configs (higher priority — later overwrites earlier)
|
|
44
|
+
for (const configPath of getProjectConfigPaths(cwd)) {
|
|
45
|
+
const config = loadConfigFile(configPath);
|
|
46
|
+
if (config) {
|
|
47
|
+
Object.assign(merged.mcpServers, config.mcpServers);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return merged;
|
|
51
|
+
}
|
|
52
|
+
// ── Connection management ──
|
|
53
|
+
const connectedServers = [];
|
|
54
|
+
export async function connectToServers(config, onStatus) {
|
|
55
|
+
const entries = Object.entries(config.mcpServers);
|
|
56
|
+
if (entries.length === 0)
|
|
57
|
+
return [];
|
|
58
|
+
for (const [name, serverConfig] of entries) {
|
|
59
|
+
try {
|
|
60
|
+
onStatus?.(name, "connecting");
|
|
61
|
+
const transport = new StdioClientTransport({
|
|
62
|
+
command: serverConfig.command,
|
|
63
|
+
args: serverConfig.args ?? [],
|
|
64
|
+
env: { ...process.env, ...(serverConfig.env ?? {}) },
|
|
65
|
+
});
|
|
66
|
+
const client = new Client({
|
|
67
|
+
name: "codemaxxing",
|
|
68
|
+
version: "0.3.0",
|
|
69
|
+
});
|
|
70
|
+
await client.connect(transport);
|
|
71
|
+
// Fetch available tools
|
|
72
|
+
const toolsResult = await client.listTools();
|
|
73
|
+
const tools = (toolsResult.tools ?? []).map((t) => ({
|
|
74
|
+
name: t.name,
|
|
75
|
+
description: t.description,
|
|
76
|
+
inputSchema: (t.inputSchema ?? { type: "object", properties: {} }),
|
|
77
|
+
}));
|
|
78
|
+
const server = { name, client, transport, tools };
|
|
79
|
+
connectedServers.push(server);
|
|
80
|
+
onStatus?.(name, `connected (${tools.length} tools)`);
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
onStatus?.(name, `failed: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return connectedServers;
|
|
87
|
+
}
|
|
88
|
+
export async function disconnectAll() {
|
|
89
|
+
for (const server of connectedServers) {
|
|
90
|
+
try {
|
|
91
|
+
await server.client.close();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Ignore cleanup errors
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
connectedServers.length = 0;
|
|
98
|
+
}
|
|
99
|
+
export function getConnectedServers() {
|
|
100
|
+
return connectedServers;
|
|
101
|
+
}
|
|
102
|
+
// ── Tool format conversion ──
|
|
103
|
+
export function getAllMCPTools(servers) {
|
|
104
|
+
const tools = [];
|
|
105
|
+
for (const server of servers) {
|
|
106
|
+
for (const tool of server.tools) {
|
|
107
|
+
tools.push({
|
|
108
|
+
type: "function",
|
|
109
|
+
function: {
|
|
110
|
+
name: `mcp_${server.name}_${tool.name}`,
|
|
111
|
+
description: `[MCP: ${server.name}] ${tool.description ?? tool.name}`,
|
|
112
|
+
parameters: tool.inputSchema,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return tools;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Parse an MCP tool call name to extract server name and tool name.
|
|
121
|
+
* Format: mcp_<serverName>_<toolName>
|
|
122
|
+
* Server names can contain hyphens but not underscores (by convention).
|
|
123
|
+
*/
|
|
124
|
+
export function parseMCPToolName(fullName) {
|
|
125
|
+
if (!fullName.startsWith("mcp_"))
|
|
126
|
+
return null;
|
|
127
|
+
const rest = fullName.slice(4); // Remove "mcp_"
|
|
128
|
+
// Find the server by matching known connected server names
|
|
129
|
+
for (const server of connectedServers) {
|
|
130
|
+
const prefix = server.name + "_";
|
|
131
|
+
if (rest.startsWith(prefix)) {
|
|
132
|
+
return { serverName: server.name, toolName: rest.slice(prefix.length) };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Fallback: split on first underscore
|
|
136
|
+
const idx = rest.indexOf("_");
|
|
137
|
+
if (idx === -1)
|
|
138
|
+
return null;
|
|
139
|
+
return { serverName: rest.slice(0, idx), toolName: rest.slice(idx + 1) };
|
|
140
|
+
}
|
|
141
|
+
// ── Tool execution ──
|
|
142
|
+
export async function callMCPTool(serverName, toolName, args) {
|
|
143
|
+
const server = connectedServers.find((s) => s.name === serverName);
|
|
144
|
+
if (!server) {
|
|
145
|
+
return `Error: MCP server "${serverName}" not found or not connected.`;
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const result = await server.client.callTool({ name: toolName, arguments: args });
|
|
149
|
+
// MCP tool results have a content array
|
|
150
|
+
const content = result.content;
|
|
151
|
+
if (Array.isArray(content)) {
|
|
152
|
+
return content
|
|
153
|
+
.map((c) => {
|
|
154
|
+
if (c.type === "text")
|
|
155
|
+
return c.text;
|
|
156
|
+
if (c.type === "image")
|
|
157
|
+
return `[image: ${c.mimeType}]`;
|
|
158
|
+
return JSON.stringify(c);
|
|
159
|
+
})
|
|
160
|
+
.join("\n");
|
|
161
|
+
}
|
|
162
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
return `Error calling MCP tool "${toolName}" on server "${serverName}": ${err.message}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// ── Server management ──
|
|
169
|
+
export function addServer(name, config) {
|
|
170
|
+
try {
|
|
171
|
+
if (!existsSync(GLOBAL_CONFIG_DIR)) {
|
|
172
|
+
mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
173
|
+
}
|
|
174
|
+
let existing = { mcpServers: {} };
|
|
175
|
+
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
176
|
+
try {
|
|
177
|
+
existing = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
178
|
+
if (!existing.mcpServers)
|
|
179
|
+
existing.mcpServers = {};
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
existing = { mcpServers: {} };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
existing.mcpServers[name] = config;
|
|
186
|
+
writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
187
|
+
return { ok: true, message: `Added MCP server "${name}" to global config.` };
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
return { ok: false, message: `Failed to add server: ${err.message}` };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export function removeServer(name) {
|
|
194
|
+
try {
|
|
195
|
+
if (!existsSync(GLOBAL_CONFIG_PATH)) {
|
|
196
|
+
return { ok: false, message: `No global MCP config found.` };
|
|
197
|
+
}
|
|
198
|
+
const existing = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
199
|
+
if (!existing.mcpServers || !existing.mcpServers[name]) {
|
|
200
|
+
return { ok: false, message: `Server "${name}" not found in global config.` };
|
|
201
|
+
}
|
|
202
|
+
delete existing.mcpServers[name];
|
|
203
|
+
writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
204
|
+
return { ok: true, message: `Removed MCP server "${name}" from global config.` };
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
return { ok: false, message: `Failed to remove server: ${err.message}` };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
export function listServers(cwd) {
|
|
211
|
+
const result = [];
|
|
212
|
+
// Gather from global config
|
|
213
|
+
const globalConfig = loadConfigFile(GLOBAL_CONFIG_PATH);
|
|
214
|
+
if (globalConfig) {
|
|
215
|
+
for (const [name, cfg] of Object.entries(globalConfig.mcpServers)) {
|
|
216
|
+
const connected = connectedServers.find((s) => s.name === name);
|
|
217
|
+
result.push({
|
|
218
|
+
name,
|
|
219
|
+
source: "global",
|
|
220
|
+
command: `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim(),
|
|
221
|
+
connected: !!connected,
|
|
222
|
+
toolCount: connected?.tools.length ?? 0,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// Gather from project configs
|
|
227
|
+
for (const configPath of getProjectConfigPaths(cwd)) {
|
|
228
|
+
const config = loadConfigFile(configPath);
|
|
229
|
+
if (config) {
|
|
230
|
+
const source = configPath.includes(".cursor") ? "cursor" : configPath.includes("opencode") ? "opencode" : "project";
|
|
231
|
+
for (const [name, cfg] of Object.entries(config.mcpServers)) {
|
|
232
|
+
// Skip if already listed from global (project overrides)
|
|
233
|
+
const existing = result.find((r) => r.name === name);
|
|
234
|
+
if (existing) {
|
|
235
|
+
existing.source = source;
|
|
236
|
+
existing.command = `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim();
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const connected = connectedServers.find((s) => s.name === name);
|
|
240
|
+
result.push({
|
|
241
|
+
name,
|
|
242
|
+
source,
|
|
243
|
+
command: `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim(),
|
|
244
|
+
connected: !!connected,
|
|
245
|
+
toolCount: connected?.tools.length ?? 0,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codemaxxing",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Open-source terminal coding agent. Connect any LLM. Max your code.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
30
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
30
31
|
"@types/react": "^19.2.14",
|
|
31
32
|
"better-sqlite3": "^12.6.2",
|
|
32
33
|
"chalk": "^5.3.0",
|
package/src/agent.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { buildProjectContext, getSystemPrompt, loadProjectRules } from "./utils/
|
|
|
11
11
|
import { isGitRepo, autoCommit } from "./utils/git.js";
|
|
12
12
|
import { buildSkillPrompts, getActiveSkillCount } from "./utils/skills.js";
|
|
13
13
|
import { createSession, saveMessage, updateTokenEstimate, updateSessionCost, loadMessages } from "./utils/sessions.js";
|
|
14
|
+
import { loadMCPConfig, connectToServers, disconnectAll, getAllMCPTools, parseMCPToolName, callMCPTool, getConnectedServers, type ConnectedServer } from "./utils/mcp.js";
|
|
14
15
|
import type { ProviderConfig } from "./config.js";
|
|
15
16
|
|
|
16
17
|
// Tools that can modify your project — require approval
|
|
@@ -74,6 +75,7 @@ export interface AgentOptions {
|
|
|
74
75
|
onContextCompressed?: (oldTokens: number, newTokens: number) => void;
|
|
75
76
|
onArchitectPlan?: (plan: string) => void;
|
|
76
77
|
onLintResult?: (file: string, errors: string) => void;
|
|
78
|
+
onMCPStatus?: (server: string, status: string) => void;
|
|
77
79
|
contextCompressionThreshold?: number;
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -108,6 +110,7 @@ export class CodingAgent {
|
|
|
108
110
|
private architectModel: string | null = null;
|
|
109
111
|
private autoLintEnabled: boolean = true;
|
|
110
112
|
private detectedLinter: { command: string; name: string } | null = null;
|
|
113
|
+
private mcpServers: ConnectedServer[] = [];
|
|
111
114
|
|
|
112
115
|
constructor(private options: AgentOptions) {
|
|
113
116
|
this.providerType = options.provider.type || "openai";
|
|
@@ -145,6 +148,16 @@ export class CodingAgent {
|
|
|
145
148
|
// Detect project linter
|
|
146
149
|
this.detectedLinter = detectLinter(this.cwd);
|
|
147
150
|
|
|
151
|
+
// Connect to MCP servers
|
|
152
|
+
const mcpConfig = loadMCPConfig(this.cwd);
|
|
153
|
+
if (Object.keys(mcpConfig.mcpServers).length > 0) {
|
|
154
|
+
this.mcpServers = await connectToServers(mcpConfig, this.options.onMCPStatus);
|
|
155
|
+
if (this.mcpServers.length > 0) {
|
|
156
|
+
const mcpTools = getAllMCPTools(this.mcpServers);
|
|
157
|
+
this.tools = [...FILE_TOOLS, ...mcpTools];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
148
161
|
this.messages = [
|
|
149
162
|
{ role: "system", content: this.systemPrompt },
|
|
150
163
|
];
|
|
@@ -357,7 +370,14 @@ export class CodingAgent {
|
|
|
357
370
|
}
|
|
358
371
|
}
|
|
359
372
|
|
|
360
|
-
|
|
373
|
+
// Route to MCP or built-in tool
|
|
374
|
+
const mcpParsed = parseMCPToolName(toolCall.name);
|
|
375
|
+
let result: string;
|
|
376
|
+
if (mcpParsed) {
|
|
377
|
+
result = await callMCPTool(mcpParsed.serverName, mcpParsed.toolName, args);
|
|
378
|
+
} else {
|
|
379
|
+
result = await executeTool(toolCall.name, args, this.cwd);
|
|
380
|
+
}
|
|
361
381
|
this.options.onToolResult?.(toolCall.name, result);
|
|
362
382
|
|
|
363
383
|
// Auto-commit after successful write_file (only if enabled)
|
|
@@ -569,7 +589,14 @@ export class CodingAgent {
|
|
|
569
589
|
}
|
|
570
590
|
}
|
|
571
591
|
|
|
572
|
-
|
|
592
|
+
// Route to MCP or built-in tool
|
|
593
|
+
const mcpParsed = parseMCPToolName(toolCall.name);
|
|
594
|
+
let result: string;
|
|
595
|
+
if (mcpParsed) {
|
|
596
|
+
result = await callMCPTool(mcpParsed.serverName, mcpParsed.toolName, args);
|
|
597
|
+
} else {
|
|
598
|
+
result = await executeTool(toolCall.name, args, this.cwd);
|
|
599
|
+
}
|
|
573
600
|
this.options.onToolResult?.(toolCall.name, result);
|
|
574
601
|
|
|
575
602
|
// Auto-commit after successful write_file
|
|
@@ -834,6 +861,32 @@ export class CodingAgent {
|
|
|
834
861
|
return this.chat(editorPrompt);
|
|
835
862
|
}
|
|
836
863
|
|
|
864
|
+
getMCPServerCount(): number {
|
|
865
|
+
return this.mcpServers.length;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
getMCPServers(): ConnectedServer[] {
|
|
869
|
+
return this.mcpServers;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
async disconnectMCP(): Promise<void> {
|
|
873
|
+
await disconnectAll();
|
|
874
|
+
this.mcpServers = [];
|
|
875
|
+
this.tools = FILE_TOOLS;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async reconnectMCP(): Promise<void> {
|
|
879
|
+
await this.disconnectMCP();
|
|
880
|
+
const mcpConfig = loadMCPConfig(this.cwd);
|
|
881
|
+
if (Object.keys(mcpConfig.mcpServers).length > 0) {
|
|
882
|
+
this.mcpServers = await connectToServers(mcpConfig, this.options.onMCPStatus);
|
|
883
|
+
if (this.mcpServers.length > 0) {
|
|
884
|
+
const mcpTools = getAllMCPTools(this.mcpServers);
|
|
885
|
+
this.tools = [...FILE_TOOLS, ...mcpTools];
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
837
890
|
reset(): void {
|
|
838
891
|
const systemMsg = this.messages[0];
|
|
839
892
|
this.messages = [systemMsg];
|
package/src/exec.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { CodingAgent } from "./agent.js";
|
|
9
9
|
import { loadConfig, applyOverrides, detectLocalProvider } from "./config.js";
|
|
10
10
|
import { getCredential } from "./utils/auth.js";
|
|
11
|
+
import { disconnectAll } from "./utils/mcp.js";
|
|
11
12
|
|
|
12
13
|
interface ExecArgs {
|
|
13
14
|
prompt: string;
|
|
@@ -140,10 +141,19 @@ export async function runExec(argv: string[]): Promise<void> {
|
|
|
140
141
|
process.stderr.write(`⚠ Denied ${name} (use --auto-approve to allow)\n`);
|
|
141
142
|
return "no";
|
|
142
143
|
},
|
|
144
|
+
onMCPStatus: (server, status) => {
|
|
145
|
+
process.stderr.write(`MCP ${server}: ${status}\n`);
|
|
146
|
+
},
|
|
143
147
|
});
|
|
144
148
|
|
|
145
149
|
try {
|
|
146
150
|
await agent.init();
|
|
151
|
+
|
|
152
|
+
const mcpCount = agent.getMCPServerCount();
|
|
153
|
+
if (mcpCount > 0) {
|
|
154
|
+
process.stderr.write(`MCP: ${mcpCount} server${mcpCount > 1 ? "s" : ""} connected\n`);
|
|
155
|
+
}
|
|
156
|
+
|
|
147
157
|
await agent.send(args.prompt);
|
|
148
158
|
|
|
149
159
|
if (!args.json) {
|
|
@@ -160,8 +170,10 @@ export async function runExec(argv: string[]): Promise<void> {
|
|
|
160
170
|
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
161
171
|
}
|
|
162
172
|
|
|
173
|
+
await disconnectAll();
|
|
163
174
|
process.exit(hasChanges ? 0 : 2);
|
|
164
175
|
} catch (err: any) {
|
|
176
|
+
await disconnectAll();
|
|
165
177
|
process.stderr.write(`Error: ${err.message}\n`);
|
|
166
178
|
if (args.json) {
|
|
167
179
|
process.stdout.write(JSON.stringify({ error: err.message }, null, 2) + "\n");
|
package/src/index.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import { isGitRepo, getBranch, getStatus, getDiff, undoLastCommit } from "./util
|
|
|
12
12
|
import { getTheme, listThemes, THEMES, DEFAULT_THEME, type Theme } from "./themes.js";
|
|
13
13
|
import { PROVIDERS, getCredentials, openRouterOAuth, anthropicSetupToken, importCodexToken, importQwenToken, copilotDeviceFlow, saveApiKey } from "./utils/auth.js";
|
|
14
14
|
import { listInstalledSkills, installSkill, removeSkill, getRegistrySkills, searchRegistry, createSkillScaffold, getActiveSkills, getActiveSkillCount } from "./utils/skills.js";
|
|
15
|
+
import { listServers, addServer, removeServer, getAllMCPTools, getConnectedServers } from "./utils/mcp.js";
|
|
15
16
|
|
|
16
17
|
const VERSION = "0.1.9";
|
|
17
18
|
|
|
@@ -58,6 +59,11 @@ const SLASH_COMMANDS = [
|
|
|
58
59
|
{ cmd: "/lint", desc: "show auto-lint status" },
|
|
59
60
|
{ cmd: "/lint on", desc: "enable auto-lint" },
|
|
60
61
|
{ cmd: "/lint off", desc: "disable auto-lint" },
|
|
62
|
+
{ cmd: "/mcp", desc: "show MCP servers" },
|
|
63
|
+
{ cmd: "/mcp tools", desc: "list MCP tools" },
|
|
64
|
+
{ cmd: "/mcp add", desc: "add MCP server" },
|
|
65
|
+
{ cmd: "/mcp remove", desc: "remove MCP server" },
|
|
66
|
+
{ cmd: "/mcp reconnect", desc: "reconnect MCP servers" },
|
|
61
67
|
{ cmd: "/quit", desc: "exit" },
|
|
62
68
|
];
|
|
63
69
|
|
|
@@ -287,6 +293,9 @@ function App() {
|
|
|
287
293
|
onLintResult: (file, errors) => {
|
|
288
294
|
addMsg("info", `🔍 Lint errors in ${file}:\n${errors}`);
|
|
289
295
|
},
|
|
296
|
+
onMCPStatus: (server, status) => {
|
|
297
|
+
addMsg("info", `🔌 MCP ${server}: ${status}`);
|
|
298
|
+
},
|
|
290
299
|
contextCompressionThreshold: config.defaults.contextCompressionThreshold,
|
|
291
300
|
onToolApproval: (name, args, diff) => {
|
|
292
301
|
return new Promise((resolve) => {
|
|
@@ -306,6 +315,13 @@ function App() {
|
|
|
306
315
|
setConnectionInfo([...info]);
|
|
307
316
|
}
|
|
308
317
|
|
|
318
|
+
// Show MCP server count
|
|
319
|
+
const mcpCount = a.getMCPServerCount();
|
|
320
|
+
if (mcpCount > 0) {
|
|
321
|
+
info.push(`🔌 ${mcpCount} MCP server${mcpCount > 1 ? "s" : ""} connected`);
|
|
322
|
+
setConnectionInfo([...info]);
|
|
323
|
+
}
|
|
324
|
+
|
|
309
325
|
setAgent(a);
|
|
310
326
|
setModelName(provider.model);
|
|
311
327
|
providerRef.current = { baseUrl: provider.baseUrl, apiKey: provider.apiKey };
|
|
@@ -419,6 +435,11 @@ function App() {
|
|
|
419
435
|
" /lint — show auto-lint status & detected linter",
|
|
420
436
|
" /lint on — enable auto-lint",
|
|
421
437
|
" /lint off — disable auto-lint",
|
|
438
|
+
" /mcp — show MCP servers & status",
|
|
439
|
+
" /mcp tools — list all MCP tools",
|
|
440
|
+
" /mcp add — add MCP server to global config",
|
|
441
|
+
" /mcp remove — remove MCP server",
|
|
442
|
+
" /mcp reconnect — reconnect all MCP servers",
|
|
422
443
|
" /quit — exit",
|
|
423
444
|
].join("\n"));
|
|
424
445
|
return;
|
|
@@ -579,6 +600,71 @@ function App() {
|
|
|
579
600
|
return;
|
|
580
601
|
}
|
|
581
602
|
|
|
603
|
+
// ── MCP commands (partially work without agent) ──
|
|
604
|
+
if (trimmed === "/mcp" || trimmed === "/mcp list") {
|
|
605
|
+
const servers = listServers(process.cwd());
|
|
606
|
+
if (servers.length === 0) {
|
|
607
|
+
addMsg("info", "🔌 No MCP servers configured.\n Add one: /mcp add <name> <command> [args...]");
|
|
608
|
+
} else {
|
|
609
|
+
const lines = servers.map((s) => {
|
|
610
|
+
const status = s.connected ? `✔ connected (${s.toolCount} tools)` : "✗ not connected";
|
|
611
|
+
return ` ${s.connected ? "●" : "○"} ${s.name} [${s.source}] — ${s.command}\n ${status}`;
|
|
612
|
+
});
|
|
613
|
+
addMsg("info", `🔌 MCP Servers:\n${lines.join("\n")}`);
|
|
614
|
+
}
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (trimmed === "/mcp tools") {
|
|
618
|
+
const servers = getConnectedServers();
|
|
619
|
+
if (servers.length === 0) {
|
|
620
|
+
addMsg("info", "🔌 No MCP servers connected.");
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const lines: string[] = [];
|
|
624
|
+
for (const server of servers) {
|
|
625
|
+
lines.push(`${server.name} (${server.tools.length} tools):`);
|
|
626
|
+
for (const tool of server.tools) {
|
|
627
|
+
lines.push(` • ${tool.name} — ${tool.description ?? "(no description)"}`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
addMsg("info", `🔌 MCP Tools:\n${lines.join("\n")}`);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
if (trimmed.startsWith("/mcp add ")) {
|
|
634
|
+
const parts = trimmed.replace("/mcp add ", "").trim().split(/\s+/);
|
|
635
|
+
if (parts.length < 2) {
|
|
636
|
+
addMsg("info", "Usage: /mcp add <name> <command> [args...]\n Example: /mcp add github npx -y @modelcontextprotocol/server-github");
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
const [name, command, ...cmdArgs] = parts;
|
|
640
|
+
const result = addServer(name, { command, args: cmdArgs.length > 0 ? cmdArgs : undefined });
|
|
641
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
if (trimmed.startsWith("/mcp remove ")) {
|
|
645
|
+
const name = trimmed.replace("/mcp remove ", "").trim();
|
|
646
|
+
if (!name) {
|
|
647
|
+
addMsg("info", "Usage: /mcp remove <name>");
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const result = removeServer(name);
|
|
651
|
+
addMsg(result.ok ? "info" : "error", result.ok ? `✅ ${result.message}` : `✗ ${result.message}`);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
if (trimmed === "/mcp reconnect") {
|
|
655
|
+
if (!agent) {
|
|
656
|
+
addMsg("info", "⚠ No agent connected. Connect first.");
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
addMsg("info", "🔌 Reconnecting MCP servers...");
|
|
660
|
+
await agent.reconnectMCP();
|
|
661
|
+
const count = agent.getMCPServerCount();
|
|
662
|
+
addMsg("info", count > 0
|
|
663
|
+
? `✅ ${count} MCP server${count > 1 ? "s" : ""} reconnected.`
|
|
664
|
+
: "No MCP servers connected.");
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
|
|
582
668
|
// Commands below require an active LLM connection
|
|
583
669
|
if (!agent) {
|
|
584
670
|
addMsg("info", "⚠ No LLM connected. Use /login to authenticate with a provider, or start a local server.");
|
package/src/utils/mcp.ts
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP (Model Context Protocol) client support
|
|
3
|
+
* Connects to external MCP servers and exposes their tools to the LLM agent.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
7
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
8
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import type { ChatCompletionTool } from "openai/resources/chat/completions";
|
|
12
|
+
|
|
13
|
+
// ── Types ──
|
|
14
|
+
|
|
15
|
+
export interface MCPServerConfig {
|
|
16
|
+
command: string;
|
|
17
|
+
args?: string[];
|
|
18
|
+
env?: Record<string, string>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface MCPConfig {
|
|
22
|
+
mcpServers: Record<string, MCPServerConfig>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ConnectedServer {
|
|
26
|
+
name: string;
|
|
27
|
+
client: Client;
|
|
28
|
+
transport: StdioClientTransport;
|
|
29
|
+
tools: Array<{ name: string; description?: string; inputSchema: Record<string, unknown> }>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ── Config paths ──
|
|
33
|
+
|
|
34
|
+
const GLOBAL_CONFIG_DIR = join(homedir(), ".codemaxxing");
|
|
35
|
+
const GLOBAL_CONFIG_PATH = join(GLOBAL_CONFIG_DIR, "mcp.json");
|
|
36
|
+
|
|
37
|
+
function getProjectConfigPaths(cwd: string): string[] {
|
|
38
|
+
return [
|
|
39
|
+
join(cwd, ".codemaxxing", "mcp.json"),
|
|
40
|
+
join(cwd, ".cursor", "mcp.json"),
|
|
41
|
+
join(cwd, "opencode.json"),
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Config loading ──
|
|
46
|
+
|
|
47
|
+
function loadConfigFile(path: string): MCPConfig | null {
|
|
48
|
+
try {
|
|
49
|
+
if (!existsSync(path)) return null;
|
|
50
|
+
const raw = readFileSync(path, "utf-8");
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (parsed.mcpServers && typeof parsed.mcpServers === "object") {
|
|
53
|
+
return parsed as MCPConfig;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function loadMCPConfig(cwd: string): MCPConfig {
|
|
62
|
+
const merged: MCPConfig = { mcpServers: {} };
|
|
63
|
+
|
|
64
|
+
// Load global config first (lower priority)
|
|
65
|
+
const globalConfig = loadConfigFile(GLOBAL_CONFIG_PATH);
|
|
66
|
+
if (globalConfig) {
|
|
67
|
+
Object.assign(merged.mcpServers, globalConfig.mcpServers);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Load project configs (higher priority — later overwrites earlier)
|
|
71
|
+
for (const configPath of getProjectConfigPaths(cwd)) {
|
|
72
|
+
const config = loadConfigFile(configPath);
|
|
73
|
+
if (config) {
|
|
74
|
+
Object.assign(merged.mcpServers, config.mcpServers);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return merged;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── Connection management ──
|
|
82
|
+
|
|
83
|
+
const connectedServers: ConnectedServer[] = [];
|
|
84
|
+
|
|
85
|
+
export async function connectToServers(
|
|
86
|
+
config: MCPConfig,
|
|
87
|
+
onStatus?: (name: string, status: string) => void,
|
|
88
|
+
): Promise<ConnectedServer[]> {
|
|
89
|
+
const entries = Object.entries(config.mcpServers);
|
|
90
|
+
if (entries.length === 0) return [];
|
|
91
|
+
|
|
92
|
+
for (const [name, serverConfig] of entries) {
|
|
93
|
+
try {
|
|
94
|
+
onStatus?.(name, "connecting");
|
|
95
|
+
|
|
96
|
+
const transport = new StdioClientTransport({
|
|
97
|
+
command: serverConfig.command,
|
|
98
|
+
args: serverConfig.args ?? [],
|
|
99
|
+
env: { ...process.env, ...(serverConfig.env ?? {}) } as Record<string, string>,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const client = new Client({
|
|
103
|
+
name: "codemaxxing",
|
|
104
|
+
version: "0.3.0",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await client.connect(transport);
|
|
108
|
+
|
|
109
|
+
// Fetch available tools
|
|
110
|
+
const toolsResult = await client.listTools();
|
|
111
|
+
const tools = (toolsResult.tools ?? []).map((t) => ({
|
|
112
|
+
name: t.name,
|
|
113
|
+
description: t.description,
|
|
114
|
+
inputSchema: (t.inputSchema ?? { type: "object", properties: {} }) as Record<string, unknown>,
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
const server: ConnectedServer = { name, client, transport, tools };
|
|
118
|
+
connectedServers.push(server);
|
|
119
|
+
onStatus?.(name, `connected (${tools.length} tools)`);
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
onStatus?.(name, `failed: ${err.message}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return connectedServers;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function disconnectAll(): Promise<void> {
|
|
129
|
+
for (const server of connectedServers) {
|
|
130
|
+
try {
|
|
131
|
+
await server.client.close();
|
|
132
|
+
} catch {
|
|
133
|
+
// Ignore cleanup errors
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
connectedServers.length = 0;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getConnectedServers(): ConnectedServer[] {
|
|
140
|
+
return connectedServers;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── Tool format conversion ──
|
|
144
|
+
|
|
145
|
+
export function getAllMCPTools(servers: ConnectedServer[]): ChatCompletionTool[] {
|
|
146
|
+
const tools: ChatCompletionTool[] = [];
|
|
147
|
+
|
|
148
|
+
for (const server of servers) {
|
|
149
|
+
for (const tool of server.tools) {
|
|
150
|
+
tools.push({
|
|
151
|
+
type: "function",
|
|
152
|
+
function: {
|
|
153
|
+
name: `mcp_${server.name}_${tool.name}`,
|
|
154
|
+
description: `[MCP: ${server.name}] ${tool.description ?? tool.name}`,
|
|
155
|
+
parameters: tool.inputSchema as any,
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return tools;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Parse an MCP tool call name to extract server name and tool name.
|
|
166
|
+
* Format: mcp_<serverName>_<toolName>
|
|
167
|
+
* Server names can contain hyphens but not underscores (by convention).
|
|
168
|
+
*/
|
|
169
|
+
export function parseMCPToolName(fullName: string): { serverName: string; toolName: string } | null {
|
|
170
|
+
if (!fullName.startsWith("mcp_")) return null;
|
|
171
|
+
const rest = fullName.slice(4); // Remove "mcp_"
|
|
172
|
+
|
|
173
|
+
// Find the server by matching known connected server names
|
|
174
|
+
for (const server of connectedServers) {
|
|
175
|
+
const prefix = server.name + "_";
|
|
176
|
+
if (rest.startsWith(prefix)) {
|
|
177
|
+
return { serverName: server.name, toolName: rest.slice(prefix.length) };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fallback: split on first underscore
|
|
182
|
+
const idx = rest.indexOf("_");
|
|
183
|
+
if (idx === -1) return null;
|
|
184
|
+
return { serverName: rest.slice(0, idx), toolName: rest.slice(idx + 1) };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Tool execution ──
|
|
188
|
+
|
|
189
|
+
export async function callMCPTool(
|
|
190
|
+
serverName: string,
|
|
191
|
+
toolName: string,
|
|
192
|
+
args: Record<string, unknown>,
|
|
193
|
+
): Promise<string> {
|
|
194
|
+
const server = connectedServers.find((s) => s.name === serverName);
|
|
195
|
+
if (!server) {
|
|
196
|
+
return `Error: MCP server "${serverName}" not found or not connected.`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const result = await server.client.callTool({ name: toolName, arguments: args });
|
|
201
|
+
// MCP tool results have a content array
|
|
202
|
+
const content = result.content;
|
|
203
|
+
if (Array.isArray(content)) {
|
|
204
|
+
return content
|
|
205
|
+
.map((c: any) => {
|
|
206
|
+
if (c.type === "text") return c.text;
|
|
207
|
+
if (c.type === "image") return `[image: ${c.mimeType}]`;
|
|
208
|
+
return JSON.stringify(c);
|
|
209
|
+
})
|
|
210
|
+
.join("\n");
|
|
211
|
+
}
|
|
212
|
+
return typeof content === "string" ? content : JSON.stringify(content);
|
|
213
|
+
} catch (err: any) {
|
|
214
|
+
return `Error calling MCP tool "${toolName}" on server "${serverName}": ${err.message}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── Server management ──
|
|
219
|
+
|
|
220
|
+
export function addServer(name: string, config: MCPServerConfig): { ok: boolean; message: string } {
|
|
221
|
+
try {
|
|
222
|
+
if (!existsSync(GLOBAL_CONFIG_DIR)) {
|
|
223
|
+
mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
let existing: MCPConfig = { mcpServers: {} };
|
|
227
|
+
if (existsSync(GLOBAL_CONFIG_PATH)) {
|
|
228
|
+
try {
|
|
229
|
+
existing = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
230
|
+
if (!existing.mcpServers) existing.mcpServers = {};
|
|
231
|
+
} catch {
|
|
232
|
+
existing = { mcpServers: {} };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
existing.mcpServers[name] = config;
|
|
237
|
+
writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
238
|
+
return { ok: true, message: `Added MCP server "${name}" to global config.` };
|
|
239
|
+
} catch (err: any) {
|
|
240
|
+
return { ok: false, message: `Failed to add server: ${err.message}` };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function removeServer(name: string): { ok: boolean; message: string } {
|
|
245
|
+
try {
|
|
246
|
+
if (!existsSync(GLOBAL_CONFIG_PATH)) {
|
|
247
|
+
return { ok: false, message: `No global MCP config found.` };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const existing: MCPConfig = JSON.parse(readFileSync(GLOBAL_CONFIG_PATH, "utf-8"));
|
|
251
|
+
if (!existing.mcpServers || !existing.mcpServers[name]) {
|
|
252
|
+
return { ok: false, message: `Server "${name}" not found in global config.` };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
delete existing.mcpServers[name];
|
|
256
|
+
writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(existing, null, 2) + "\n", "utf-8");
|
|
257
|
+
return { ok: true, message: `Removed MCP server "${name}" from global config.` };
|
|
258
|
+
} catch (err: any) {
|
|
259
|
+
return { ok: false, message: `Failed to remove server: ${err.message}` };
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function listServers(cwd: string): Array<{ name: string; source: string; command: string; connected: boolean; toolCount: number }> {
|
|
264
|
+
const result: Array<{ name: string; source: string; command: string; connected: boolean; toolCount: number }> = [];
|
|
265
|
+
|
|
266
|
+
// Gather from global config
|
|
267
|
+
const globalConfig = loadConfigFile(GLOBAL_CONFIG_PATH);
|
|
268
|
+
if (globalConfig) {
|
|
269
|
+
for (const [name, cfg] of Object.entries(globalConfig.mcpServers)) {
|
|
270
|
+
const connected = connectedServers.find((s) => s.name === name);
|
|
271
|
+
result.push({
|
|
272
|
+
name,
|
|
273
|
+
source: "global",
|
|
274
|
+
command: `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim(),
|
|
275
|
+
connected: !!connected,
|
|
276
|
+
toolCount: connected?.tools.length ?? 0,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Gather from project configs
|
|
282
|
+
for (const configPath of getProjectConfigPaths(cwd)) {
|
|
283
|
+
const config = loadConfigFile(configPath);
|
|
284
|
+
if (config) {
|
|
285
|
+
const source = configPath.includes(".cursor") ? "cursor" : configPath.includes("opencode") ? "opencode" : "project";
|
|
286
|
+
for (const [name, cfg] of Object.entries(config.mcpServers)) {
|
|
287
|
+
// Skip if already listed from global (project overrides)
|
|
288
|
+
const existing = result.find((r) => r.name === name);
|
|
289
|
+
if (existing) {
|
|
290
|
+
existing.source = source;
|
|
291
|
+
existing.command = `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim();
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const connected = connectedServers.find((s) => s.name === name);
|
|
295
|
+
result.push({
|
|
296
|
+
name,
|
|
297
|
+
source,
|
|
298
|
+
command: `${cfg.command} ${(cfg.args ?? []).join(" ")}`.trim(),
|
|
299
|
+
connected: !!connected,
|
|
300
|
+
toolCount: connected?.tools.length ?? 0,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return result;
|
|
307
|
+
}
|