min-agent 0.1.0 → 0.1.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 (56) hide show
  1. package/README.md +6 -0
  2. package/bin/min-agent.js +2 -2
  3. package/dist/agent.js +566 -0
  4. package/dist/assistant-stream.js +114 -0
  5. package/dist/cli.js +471 -0
  6. package/dist/compaction.js +99 -0
  7. package/dist/config.js +142 -0
  8. package/dist/confirm.js +37 -0
  9. package/dist/instructions.js +115 -0
  10. package/dist/markdown.js +130 -0
  11. package/dist/mcp.js +237 -0
  12. package/dist/memory.js +131 -0
  13. package/dist/output.js +52 -0
  14. package/dist/plugins.js +66 -0
  15. package/dist/provider.js +41 -0
  16. package/dist/serve.js +351 -0
  17. package/dist/sessions.js +74 -0
  18. package/dist/skills.js +127 -0
  19. package/dist/tool-output.js +119 -0
  20. package/dist/tools/bash.js +93 -0
  21. package/dist/tools/edit.js +51 -0
  22. package/dist/tools/glob.js +36 -0
  23. package/dist/tools/grep.js +35 -0
  24. package/dist/tools/index.js +20 -0
  25. package/dist/tools/read.js +36 -0
  26. package/dist/tools/web_fetch.js +83 -0
  27. package/dist/tools/web_search.js +40 -0
  28. package/dist/tools/write.js +32 -0
  29. package/package.json +4 -5
  30. package/src/agent.ts +0 -609
  31. package/src/assistant-stream.ts +0 -128
  32. package/src/cli.ts +0 -494
  33. package/src/compaction.ts +0 -119
  34. package/src/config.ts +0 -172
  35. package/src/confirm.ts +0 -42
  36. package/src/instructions.ts +0 -123
  37. package/src/markdown.ts +0 -140
  38. package/src/mcp.ts +0 -300
  39. package/src/memory.ts +0 -164
  40. package/src/output.ts +0 -58
  41. package/src/plugins.ts +0 -94
  42. package/src/provider.ts +0 -50
  43. package/src/serve.ts +0 -400
  44. package/src/sessions.ts +0 -94
  45. package/src/skills.ts +0 -146
  46. package/src/tool-output.ts +0 -146
  47. package/src/tools/bash.ts +0 -108
  48. package/src/tools/edit.ts +0 -65
  49. package/src/tools/glob.ts +0 -37
  50. package/src/tools/grep.ts +0 -37
  51. package/src/tools/index.ts +0 -21
  52. package/src/tools/read.ts +0 -38
  53. package/src/tools/web_fetch.ts +0 -87
  54. package/src/tools/web_search.ts +0 -42
  55. package/src/tools/write.ts +0 -36
  56. package/tsconfig.json +0 -15
package/dist/config.js ADDED
@@ -0,0 +1,142 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import readline from "readline";
5
+ const CONFIG_DIR = path.join(os.homedir(), ".min-agent");
6
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
7
+ const RULES_FILE = path.join(CONFIG_DIR, "rules.md");
8
+ export function getConfigDir() {
9
+ return CONFIG_DIR;
10
+ }
11
+ export function getRulesFile() {
12
+ return RULES_FILE;
13
+ }
14
+ export function loadConfig() {
15
+ if (!existsSync(CONFIG_FILE))
16
+ return {};
17
+ try {
18
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
19
+ }
20
+ catch {
21
+ return {};
22
+ }
23
+ }
24
+ export function saveConfig(config) {
25
+ mkdirSync(CONFIG_DIR, { recursive: true });
26
+ writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
27
+ }
28
+ export function isConfigured() {
29
+ const config = loadConfig();
30
+ return !!(config.provider?.baseURL && config.provider?.apiKey);
31
+ }
32
+ async function fetchModelsFromURL(url, apiKey) {
33
+ const noCacheURL = new URL(url);
34
+ noCacheURL.searchParams.set("_t", Date.now().toString());
35
+ const response = await fetch(noCacheURL.toString(), {
36
+ headers: {
37
+ Authorization: `Bearer ${apiKey}`,
38
+ "Cache-Control": "no-cache, no-store, must-revalidate",
39
+ Pragma: "no-cache",
40
+ Expires: "0",
41
+ },
42
+ cache: "no-store",
43
+ signal: AbortSignal.timeout(10000),
44
+ });
45
+ if (!response.ok)
46
+ return [];
47
+ const data = (await response.json());
48
+ const models = (data.data ?? data ?? []);
49
+ return models.map((m) => m.id).sort();
50
+ }
51
+ function ask(rl, question, defaultValue) {
52
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
53
+ return new Promise((resolve) => {
54
+ rl.question(`${question}${suffix}: `, (answer) => {
55
+ resolve(answer.trim() || defaultValue || "");
56
+ });
57
+ });
58
+ }
59
+ export async function fetchModels(baseURL, apiKey) {
60
+ try {
61
+ const trimmed = baseURL.replace(/\/$/, "");
62
+ const primary = await fetchModelsFromURL(`${trimmed}/models`, apiKey);
63
+ if (primary.length > 0)
64
+ return primary;
65
+ // Ollama users often provide host without /v1; auto-retry that variant.
66
+ if (!trimmed.endsWith("/v1")) {
67
+ return await fetchModelsFromURL(`${trimmed}/v1/models`, apiKey);
68
+ }
69
+ return [];
70
+ }
71
+ catch {
72
+ return [];
73
+ }
74
+ }
75
+ export async function runSetup() {
76
+ const config = loadConfig();
77
+ const existing = config.provider;
78
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
79
+ console.log("\n🔧 min-agent 配置\n");
80
+ console.log("Provider 类型:");
81
+ console.log(" 1. openai-compatible (默认,兼容 OpenAI API 的任意服务)");
82
+ console.log(" 2. openai (OpenAI 官方)");
83
+ console.log(" 3. ollama (本地 Ollama)");
84
+ console.log();
85
+ const typeChoice = await ask(rl, "选择 Provider (1/2/3)", existing?.type === "ollama" ? "3" : existing?.type === "openai" ? "2" : "1");
86
+ const providerType = typeChoice === "3" ? "ollama"
87
+ : typeChoice === "2" ? "openai"
88
+ : "openai-compatible";
89
+ let baseURL;
90
+ let apiKey;
91
+ if (providerType === "ollama") {
92
+ baseURL = await ask(rl, "Ollama API URL", existing?.baseURL || "http://localhost:11434/v1");
93
+ if (!baseURL.replace(/\/$/, "").endsWith("/v1")) {
94
+ baseURL = `${baseURL.replace(/\/$/, "")}/v1`;
95
+ }
96
+ apiKey = "ollama"; // Ollama doesn't need a real key
97
+ }
98
+ else if (providerType === "openai") {
99
+ baseURL = "https://api.openai.com/v1";
100
+ apiKey = await ask(rl, "OpenAI API Key", existing?.apiKey);
101
+ }
102
+ else {
103
+ baseURL = await ask(rl, "API Base URL", existing?.baseURL || "https://api.openai.com/v1");
104
+ apiKey = await ask(rl, "API Key", existing?.apiKey);
105
+ }
106
+ if (!baseURL || (!apiKey && providerType !== "ollama")) {
107
+ console.error("URL 和 Key 不能为空");
108
+ rl.close();
109
+ process.exit(1);
110
+ }
111
+ console.log("\n正在获取模型列表...");
112
+ const models = await fetchModels(baseURL, apiKey);
113
+ let defaultModel = existing?.defaultModel ?? "";
114
+ if (models.length > 0) {
115
+ console.log(`\n可用模型 (${models.length}):`);
116
+ models.forEach((m, i) => {
117
+ const marker = m === defaultModel ? " ← 当前默认" : "";
118
+ console.log(` ${i + 1}. ${m}${marker}`);
119
+ });
120
+ console.log();
121
+ const choice = await ask(rl, "选择默认模型 (输入序号或模型名)", defaultModel);
122
+ const idx = parseInt(choice) - 1;
123
+ if (idx >= 0 && idx < models.length) {
124
+ defaultModel = models[idx];
125
+ }
126
+ else if (choice) {
127
+ defaultModel = choice;
128
+ }
129
+ }
130
+ else {
131
+ console.log(" ⚠ 无法获取模型列表,请手动输入模型名");
132
+ const hint = providerType === "ollama" ? "llama3" : providerType === "openai" ? "gpt-4o" : "";
133
+ defaultModel = await ask(rl, "默认模型", defaultModel || hint);
134
+ }
135
+ rl.close();
136
+ config.provider = { type: providerType, baseURL, apiKey, defaultModel };
137
+ saveConfig(config);
138
+ console.log(`\n✓ 配置已保存到 ${CONFIG_FILE}`);
139
+ console.log(` Type: ${providerType}`);
140
+ console.log(` URL: ${baseURL}`);
141
+ console.log(` Model: ${defaultModel}\n`);
142
+ }
@@ -0,0 +1,37 @@
1
+ import readline from "readline";
2
+ let autoApprove = false;
3
+ export function setAutoApprove(value) {
4
+ autoApprove = value;
5
+ }
6
+ export function isAutoApprove() {
7
+ return autoApprove;
8
+ }
9
+ /** Ask user for confirmation. Returns true if approved. */
10
+ export async function confirm(message) {
11
+ if (autoApprove)
12
+ return true;
13
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
14
+ return new Promise((resolve) => {
15
+ rl.question(`\x1b[33m⚠ ${message} [y/N] \x1b[0m`, (answer) => {
16
+ rl.close();
17
+ resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
18
+ });
19
+ });
20
+ }
21
+ /** Check if a shell command is potentially dangerous */
22
+ export function isDangerousCommand(command) {
23
+ const dangerous = [
24
+ /\brm\s+(-rf?|--recursive)\s/,
25
+ /\brm\s+-[a-z]*f/,
26
+ /\bsudo\b/,
27
+ /\bmkfs\b/,
28
+ /\bdd\s+/,
29
+ /\b(shutdown|reboot|halt|poweroff)\b/,
30
+ /\bgit\s+(push|reset\s+--hard|clean\s+-[a-z]*f)/,
31
+ /\bnpm\s+publish\b/,
32
+ /\bdrop\s+(table|database)\b/i,
33
+ /\btruncate\s+table\b/i,
34
+ /\bformat\b.*\b[a-z]:\b/i,
35
+ ];
36
+ return dangerous.some((re) => re.test(command));
37
+ }
@@ -0,0 +1,115 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { getRulesFile, loadConfig } from "./config.js";
5
+ /**
6
+ * Instruction/rules system.
7
+ *
8
+ * All config lives under ~/.min-agent/:
9
+ * ~/.min-agent/rules.md — Global rules (always loaded)
10
+ * ~/.min-agent/config.json — Can specify extra "instructions" paths/URLs
11
+ *
12
+ * Project-level rules (auto-discovered from cwd):
13
+ * ./AGENTS.md, ./RULES.md, ./.min-agent/AGENTS.md
14
+ */
15
+ const PROJECT_FILES = ["AGENTS.md", "RULES.md", "CLAUDE.md"];
16
+ function findProjectInstructions() {
17
+ const results = [];
18
+ let current = process.cwd();
19
+ const root = path.parse(current).root;
20
+ while (current !== root) {
21
+ // Check .min-agent/AGENTS.md in project
22
+ const dotDir = path.join(current, ".min-agent", "AGENTS.md");
23
+ if (existsSync(dotDir)) {
24
+ results.push(dotDir);
25
+ break;
26
+ }
27
+ for (const file of PROJECT_FILES) {
28
+ const filepath = path.join(current, file);
29
+ if (existsSync(filepath)) {
30
+ results.push(filepath);
31
+ break;
32
+ }
33
+ }
34
+ if (results.length > 0)
35
+ break;
36
+ current = path.dirname(current);
37
+ }
38
+ return results;
39
+ }
40
+ function findGlobalRules() {
41
+ const results = [];
42
+ const rulesFile = getRulesFile();
43
+ if (existsSync(rulesFile))
44
+ results.push(rulesFile);
45
+ return results;
46
+ }
47
+ function resolveConfigInstructions() {
48
+ const config = loadConfig();
49
+ const instructions = config.instructions ?? [];
50
+ const results = [];
51
+ for (const item of instructions) {
52
+ if (item.startsWith("http://") || item.startsWith("https://"))
53
+ continue;
54
+ const resolved = item.startsWith("~/")
55
+ ? path.join(os.homedir(), item.slice(2))
56
+ : path.isAbsolute(item)
57
+ ? item
58
+ : path.resolve(process.cwd(), item);
59
+ if (existsSync(resolved))
60
+ results.push(resolved);
61
+ }
62
+ return results;
63
+ }
64
+ async function fetchRemoteInstructions() {
65
+ const config = loadConfig();
66
+ const instructions = config.instructions ?? [];
67
+ const results = [];
68
+ const urls = instructions.filter((i) => i.startsWith("http://") || i.startsWith("https://"));
69
+ for (const url of urls) {
70
+ try {
71
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
72
+ if (response.ok) {
73
+ const text = await response.text();
74
+ if (text.trim())
75
+ results.push(`Instructions from: ${url}\n${text}`);
76
+ }
77
+ }
78
+ catch { }
79
+ }
80
+ return results;
81
+ }
82
+ export async function loadInstructions() {
83
+ const parts = [];
84
+ // Global rules from ~/.min-agent/rules.md
85
+ for (const filepath of findGlobalRules()) {
86
+ try {
87
+ const content = readFileSync(filepath, "utf-8").trim();
88
+ if (content)
89
+ parts.push(`Instructions from: ${filepath}\n${content}`);
90
+ }
91
+ catch { }
92
+ }
93
+ // Project-level instructions
94
+ for (const filepath of findProjectInstructions()) {
95
+ try {
96
+ const content = readFileSync(filepath, "utf-8").trim();
97
+ if (content)
98
+ parts.push(`Instructions from: ${filepath}\n${content}`);
99
+ }
100
+ catch { }
101
+ }
102
+ // Config-defined file instructions
103
+ for (const filepath of resolveConfigInstructions()) {
104
+ try {
105
+ const content = readFileSync(filepath, "utf-8").trim();
106
+ if (content)
107
+ parts.push(`Instructions from: ${filepath}\n${content}`);
108
+ }
109
+ catch { }
110
+ }
111
+ // Remote URL instructions
112
+ const remote = await fetchRemoteInstructions();
113
+ parts.push(...remote);
114
+ return parts;
115
+ }
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Lightweight streaming markdown renderer for terminal.
3
+ * Tracks state across text deltas to apply ANSI formatting.
4
+ */
5
+ function useColor() {
6
+ if ("NO_COLOR" in process.env)
7
+ return false;
8
+ if (process.env.FORCE_COLOR === "0")
9
+ return false;
10
+ return true;
11
+ }
12
+ const C = {
13
+ reset: "\x1b[0m",
14
+ bold: "\x1b[1m",
15
+ dim: "\x1b[2m",
16
+ italic: "\x1b[3m",
17
+ cyan: "\x1b[36m",
18
+ green: "\x1b[32m",
19
+ yellow: "\x1b[33m",
20
+ magenta: "\x1b[35m",
21
+ gray: "\x1b[90m",
22
+ underline: "\x1b[4m",
23
+ };
24
+ const Z = {
25
+ reset: "",
26
+ bold: "",
27
+ dim: "",
28
+ italic: "",
29
+ cyan: "",
30
+ green: "",
31
+ yellow: "",
32
+ magenta: "",
33
+ gray: "",
34
+ underline: "",
35
+ };
36
+ export class MarkdownRenderer {
37
+ buffer = "";
38
+ inCodeBlock = false;
39
+ codeLang = "";
40
+ /** Process a text delta and return formatted output */
41
+ write(text) {
42
+ this.buffer += text;
43
+ let output = "";
44
+ const c = useColor() ? C : Z;
45
+ // Process complete lines
46
+ while (true) {
47
+ const nlIdx = this.buffer.indexOf("\n");
48
+ if (nlIdx === -1)
49
+ break;
50
+ const line = this.buffer.slice(0, nlIdx);
51
+ this.buffer = this.buffer.slice(nlIdx + 1);
52
+ output += this.formatLine(line, c) + "\n";
53
+ }
54
+ return output;
55
+ }
56
+ /** Flush remaining buffer */
57
+ flush() {
58
+ if (!this.buffer)
59
+ return "";
60
+ const c = useColor() ? C : Z;
61
+ const out = this.formatLine(this.buffer, c);
62
+ this.buffer = "";
63
+ return out;
64
+ }
65
+ formatInline(line, c) {
66
+ // Split by inline code spans; format outside segments only
67
+ const parts = line.split(/(`[^`]*`)/g);
68
+ return parts
69
+ .map((seg) => {
70
+ if (seg.startsWith("`") && seg.endsWith("`") && seg.length >= 2) {
71
+ const inner = seg.slice(1, -1);
72
+ return `${c.cyan}${inner}${c.reset}`;
73
+ }
74
+ let s = seg;
75
+ s = s.replace(/\*\*([^*]+)\*\*/g, `${c.bold}$1${c.reset}`);
76
+ s = s.replace(/\*([^*]+)\*/g, `${c.italic}$1${c.reset}`);
77
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, `${c.cyan}$1${c.reset} ${c.dim}($2)${c.reset}`);
78
+ return s;
79
+ })
80
+ .join("");
81
+ }
82
+ formatLine(line, c) {
83
+ // Code block fence
84
+ if (line.startsWith("```")) {
85
+ if (!this.inCodeBlock) {
86
+ this.inCodeBlock = true;
87
+ this.codeLang = line.slice(3).trim();
88
+ return `${c.dim}┌─ ${this.codeLang || "code"} ${"─".repeat(Math.max(0, 40 - (this.codeLang?.length ?? 0)))}${c.reset}`;
89
+ }
90
+ this.inCodeBlock = false;
91
+ this.codeLang = "";
92
+ return `${c.dim}└${"─".repeat(44)}${c.reset}`;
93
+ }
94
+ // Inside code block — dim
95
+ if (this.inCodeBlock) {
96
+ return `${c.dim}│${c.reset} ${line}`;
97
+ }
98
+ // Headers
99
+ if (line.startsWith("### "))
100
+ return `${c.bold}${this.formatInline(line.slice(4), c)}${c.reset}`;
101
+ if (line.startsWith("## "))
102
+ return `${c.bold}${this.formatInline(line.slice(3), c)}${c.reset}`;
103
+ if (line.startsWith("# "))
104
+ return `${c.bold}${c.cyan}${this.formatInline(line.slice(2), c)}${c.reset}`;
105
+ // Horizontal rule
106
+ if (/^---+$/.test(line))
107
+ return `${c.dim}${"─".repeat(44)}${c.reset}`;
108
+ // Blockquote
109
+ const bq = line.match(/^(\s*)>\s?(.*)$/);
110
+ if (bq) {
111
+ const indent = bq[1];
112
+ const body = bq[2];
113
+ return `${indent}${c.dim}▎${c.reset} ${this.formatInline(body, c)}`;
114
+ }
115
+ // Numbered list (1. item)
116
+ if (/^\s*\d+\.\s/.test(line)) {
117
+ const m = line.match(/^(\s*)(\d+\.)(\s)(.*)$/);
118
+ if (m) {
119
+ return `${m[1]}${c.yellow}${m[2]}${c.reset}${m[3]}${this.formatInline(m[4], c)}`;
120
+ }
121
+ }
122
+ // Bullet points
123
+ if (line.match(/^\s*[-*]\s/)) {
124
+ return line.replace(/^(\s*)([-*])(\s)(.*)$/, (_a, sp, _mark, sp2, rest) => {
125
+ return `${sp}${c.cyan}•${c.reset}${sp2}${this.formatInline(rest, c)}`;
126
+ });
127
+ }
128
+ return this.formatInline(line, c);
129
+ }
130
+ }
package/dist/mcp.js ADDED
@@ -0,0 +1,237 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
5
+ import { tool, jsonSchema } from "ai";
6
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "fs";
7
+ import path from "path";
8
+ import { truncateToolOutput } from "./tool-output.js";
9
+ const DEFAULT_TIMEOUT = 30000;
10
+ function getMcpConfigPath() {
11
+ return path.join(process.cwd(), ".min-agent", "mcp.json");
12
+ }
13
+ let connectedServers = {};
14
+ export function loadMcpConfig() {
15
+ const configPath = getMcpConfigPath();
16
+ if (!existsSync(configPath))
17
+ return { mcpServers: {} };
18
+ try {
19
+ return JSON.parse(readFileSync(configPath, "utf-8"));
20
+ }
21
+ catch {
22
+ return { mcpServers: {} };
23
+ }
24
+ }
25
+ export function saveMcpConfig(config) {
26
+ const configPath = getMcpConfigPath();
27
+ const dir = path.dirname(configPath);
28
+ mkdirSync(dir, { recursive: true });
29
+ writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
30
+ }
31
+ export function isRemoteMcpConfig(config) {
32
+ return typeof config.url === "string" && config.url.trim().length > 0;
33
+ }
34
+ function buildRemoteRequestInit(config) {
35
+ const headers = new Headers(config.headers ?? {});
36
+ const token = config.token?.trim();
37
+ if (token && !headers.has("Authorization")) {
38
+ headers.set("Authorization", `Bearer ${token}`);
39
+ }
40
+ if ([...headers.keys()].length === 0)
41
+ return undefined;
42
+ return { headers };
43
+ }
44
+ async function openStdioMcpServer(name, config) {
45
+ const [cmd, ...args] = config.command ?? [];
46
+ if (!cmd) {
47
+ throw new Error(`MCP "${name}" has empty command`);
48
+ }
49
+ const transport = new StdioClientTransport({
50
+ command: cmd,
51
+ args,
52
+ env: { ...process.env, ...(config.environment ?? {}) },
53
+ stderr: "pipe",
54
+ });
55
+ const client = new Client({ name: "agent-demo", version: "0.0.1" });
56
+ await client.connect(transport);
57
+ const { tools } = await client.listTools();
58
+ return { client, transport, tools };
59
+ }
60
+ async function openRemoteMcpServer(name, config) {
61
+ const rawUrl = config.url?.trim();
62
+ if (!rawUrl) {
63
+ throw new Error(`MCP "${name}" has empty url`);
64
+ }
65
+ let baseUrl;
66
+ try {
67
+ baseUrl = new URL(rawUrl);
68
+ }
69
+ catch {
70
+ throw new Error(`MCP "${name}" has invalid url: ${rawUrl}`);
71
+ }
72
+ if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
73
+ throw new Error(`MCP "${name}" url must be http or https`);
74
+ }
75
+ const requestInit = buildRemoteRequestInit(config);
76
+ const mode = config.remoteTransport ?? "auto";
77
+ const connectStreamable = async () => {
78
+ const client = new Client({ name: "agent-demo", version: "0.0.1" });
79
+ const transport = new StreamableHTTPClientTransport(baseUrl, requestInit ? { requestInit } : undefined);
80
+ await client.connect(transport);
81
+ const { tools } = await client.listTools();
82
+ return { client, transport, tools };
83
+ };
84
+ const connectSse = async () => {
85
+ const client = new Client({ name: "agent-demo", version: "0.0.1" });
86
+ const transport = new SSEClientTransport(baseUrl, requestInit ? { requestInit } : undefined);
87
+ await client.connect(transport);
88
+ const { tools } = await client.listTools();
89
+ return { client, transport, tools };
90
+ };
91
+ if (mode === "streamable-http") {
92
+ return await connectStreamable();
93
+ }
94
+ if (mode === "sse") {
95
+ return await connectSse();
96
+ }
97
+ // auto: prefer streamable-http, fall back to sse (older servers)
98
+ try {
99
+ return await connectStreamable();
100
+ }
101
+ catch (firstErr) {
102
+ try {
103
+ return await connectSse();
104
+ }
105
+ catch {
106
+ throw new Error(`MCP "${name}" remote connection failed (streamable-http + sse): ${firstErr?.message ?? String(firstErr)}`);
107
+ }
108
+ }
109
+ }
110
+ async function openMcpServer(name, config) {
111
+ if (isRemoteMcpConfig(config)) {
112
+ return await openRemoteMcpServer(name, config);
113
+ }
114
+ return await openStdioMcpServer(name, config);
115
+ }
116
+ /** One-line summary for CLI / logs (no secrets). */
117
+ export function formatMcpServerBinding(config) {
118
+ if (isRemoteMcpConfig(config)) {
119
+ const mode = config.remoteTransport ?? "auto";
120
+ return `${config.url} [remote:${mode}]`;
121
+ }
122
+ return (config.command ?? []).join(" ");
123
+ }
124
+ export async function connectMcpServer(name, config) {
125
+ if (config.enabled === false)
126
+ return null;
127
+ try {
128
+ const server = await openMcpServer(name, config);
129
+ console.log(`\x1b[90m MCP "${name}" connected (${server.tools.length} tools)\x1b[0m`);
130
+ return server;
131
+ }
132
+ catch (err) {
133
+ console.error(`\x1b[31m MCP "${name}" failed: ${err.message}\x1b[0m`);
134
+ return null;
135
+ }
136
+ }
137
+ export async function checkMcpServer(name, config) {
138
+ if (config.enabled === false) {
139
+ return { name, enabled: false, ok: true, toolCount: 0 };
140
+ }
141
+ try {
142
+ const server = await openMcpServer(name, config);
143
+ const toolCount = server.tools.length;
144
+ try {
145
+ await server.client.close();
146
+ }
147
+ catch { }
148
+ return { name, enabled: true, ok: true, toolCount };
149
+ }
150
+ catch (err) {
151
+ return {
152
+ name,
153
+ enabled: true,
154
+ ok: false,
155
+ toolCount: 0,
156
+ error: err?.message ?? String(err),
157
+ };
158
+ }
159
+ }
160
+ export async function checkAllMcpServers() {
161
+ const config = loadMcpConfig();
162
+ const results = [];
163
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
164
+ results.push(await checkMcpServer(name, serverConfig));
165
+ }
166
+ return results;
167
+ }
168
+ export async function initMcp() {
169
+ const config = loadMcpConfig();
170
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
171
+ const server = await connectMcpServer(name, serverConfig);
172
+ if (server)
173
+ connectedServers[name] = server;
174
+ }
175
+ }
176
+ export async function shutdownMcp() {
177
+ for (const [name, server] of Object.entries(connectedServers)) {
178
+ try {
179
+ await server.client.close();
180
+ }
181
+ catch { }
182
+ }
183
+ connectedServers = {};
184
+ }
185
+ export function getMcpTools() {
186
+ const tools = {};
187
+ for (const [serverName, server] of Object.entries(connectedServers)) {
188
+ for (const mcpTool of server.tools) {
189
+ const toolId = `${sanitize(serverName)}_${sanitize(mcpTool.name)}`;
190
+ const schema = {
191
+ ...mcpTool.inputSchema,
192
+ type: "object",
193
+ properties: (mcpTool.inputSchema?.properties ?? {}),
194
+ };
195
+ tools[toolId] = tool({
196
+ description: mcpTool.description ?? `MCP tool: ${mcpTool.name}`,
197
+ inputSchema: jsonSchema(schema),
198
+ execute: async (args) => {
199
+ try {
200
+ const result = await server.client.callTool({
201
+ name: mcpTool.name,
202
+ arguments: args,
203
+ });
204
+ if (result.isError) {
205
+ return truncateToolOutput(`Error: ${JSON.stringify(result.content)}`, { direction: "head" }).content;
206
+ }
207
+ const content = result.content;
208
+ const text = content
209
+ .filter((c) => c.type === "text")
210
+ .map((c) => c.text ?? "")
211
+ .join("\n") || JSON.stringify(result.content);
212
+ return truncateToolOutput(text, { direction: "head" }).content;
213
+ }
214
+ catch (err) {
215
+ return truncateToolOutput(`MCP tool error: ${err.message}`, { direction: "head" }).content;
216
+ }
217
+ },
218
+ });
219
+ }
220
+ }
221
+ return tools;
222
+ }
223
+ export function getMcpStatus() {
224
+ const status = {};
225
+ const config = loadMcpConfig();
226
+ for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
227
+ const server = connectedServers[name];
228
+ status[name] = {
229
+ connected: !!server,
230
+ tools: server?.tools.map((t) => t.name) ?? [],
231
+ };
232
+ }
233
+ return status;
234
+ }
235
+ function sanitize(s) {
236
+ return s.replace(/[^a-zA-Z0-9_-]/g, "_");
237
+ }