@townco/cli 0.1.27 → 0.1.29
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/commands/run.js +20 -8
- package/dist/components/LogsPane.js +34 -19
- package/dist/components/MergedLogsPane.js +5 -15
- package/dist/components/StatusLine.d.ts +3 -1
- package/dist/components/StatusLine.js +2 -2
- package/dist/components/TabbedOutput.d.ts +1 -0
- package/dist/components/TabbedOutput.js +5 -7
- package/dist/index.js +0 -0
- package/package.json +7 -9
- package/dist/commands/mcp-add.d.ts +0 -14
- package/dist/commands/mcp-add.js +0 -494
- package/dist/commands/mcp-list.d.ts +0 -3
- package/dist/commands/mcp-list.js +0 -63
- package/dist/commands/mcp-remove.d.ts +0 -3
- package/dist/commands/mcp-remove.js +0 -120
- package/dist/lib/mcp-storage.d.ts +0 -32
- package/dist/lib/mcp-storage.js +0 -111
package/dist/commands/run.js
CHANGED
|
@@ -4,9 +4,9 @@ import { existsSync } from "node:fs";
|
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { isInsideTownProject } from "@townco/agent/storage";
|
|
7
|
-
import { createLogger } from "@townco/
|
|
7
|
+
import { configureLogsDir, createLogger } from "@townco/core";
|
|
8
8
|
import { AcpClient } from "@townco/ui";
|
|
9
|
-
import { ChatView } from "@townco/ui/tui";
|
|
9
|
+
import { ChatView, ChatViewStatus } from "@townco/ui/tui";
|
|
10
10
|
import { Box, render, Text } from "ink";
|
|
11
11
|
import open from "open";
|
|
12
12
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
@@ -16,14 +16,20 @@ import { findAvailablePort } from "../lib/port-utils.js";
|
|
|
16
16
|
function TuiRunner({ agentPath, workingDir, onExit }) {
|
|
17
17
|
const [client, setClient] = useState(null);
|
|
18
18
|
const [error, setError] = useState(null);
|
|
19
|
+
// Configure logs directory for UI package loggers BEFORE any loggers are created
|
|
20
|
+
// This must run synchronously before any component initialization
|
|
21
|
+
useMemo(() => {
|
|
22
|
+
configureLogsDir(join(workingDir, ".logs"));
|
|
23
|
+
}, [workingDir]);
|
|
19
24
|
// Session start time for log filtering - set once when component mounts
|
|
25
|
+
// Set to 5 seconds in the past to capture logs generated during initialization
|
|
20
26
|
const sessionStartTime = useMemo(() => {
|
|
21
27
|
const now = new Date();
|
|
22
|
-
now.setSeconds(now.getSeconds() -
|
|
28
|
+
now.setSeconds(now.getSeconds() - 5);
|
|
23
29
|
return now.toISOString();
|
|
24
30
|
}, []);
|
|
25
31
|
// Create logger with agent directory as logs location
|
|
26
|
-
const logger = useMemo(() => createLogger("tui", "debug"
|
|
32
|
+
const logger = useMemo(() => createLogger("tui", "debug"), []);
|
|
27
33
|
useEffect(() => {
|
|
28
34
|
try {
|
|
29
35
|
logger.info("Creating ACP client", { agentPath, workingDir });
|
|
@@ -62,6 +68,11 @@ function TuiRunner({ agentPath, workingDir, onExit }) {
|
|
|
62
68
|
}
|
|
63
69
|
return _jsx(ChatView, { client: client });
|
|
64
70
|
},
|
|
71
|
+
renderStatus: () => {
|
|
72
|
+
if (!client || error)
|
|
73
|
+
return null;
|
|
74
|
+
return _jsx(ChatViewStatus, { client: client });
|
|
75
|
+
},
|
|
65
76
|
},
|
|
66
77
|
{
|
|
67
78
|
name: "Logs",
|
|
@@ -158,10 +169,7 @@ export async function runCommand(options) {
|
|
|
158
169
|
}
|
|
159
170
|
const binPath = join(agentPath, "bin.ts");
|
|
160
171
|
// Create logger with agent directory as logs location
|
|
161
|
-
const logger = createLogger("cli", "debug"
|
|
162
|
-
silent: true,
|
|
163
|
-
logsDir: agentPath,
|
|
164
|
-
});
|
|
172
|
+
const logger = createLogger("cli", "debug");
|
|
165
173
|
logger.info("Starting agent", {
|
|
166
174
|
name,
|
|
167
175
|
mode: gui ? "gui" : http ? "http" : "tui",
|
|
@@ -274,6 +282,10 @@ export async function runCommand(options) {
|
|
|
274
282
|
}
|
|
275
283
|
// Default: Start TUI interface with the agent
|
|
276
284
|
logger.info("Starting TUI mode", { name, agentPath });
|
|
285
|
+
// Clear terminal and move cursor to top
|
|
286
|
+
if (process.stdout.isTTY) {
|
|
287
|
+
process.stdout.write("\x1b[2J\x1b[H");
|
|
288
|
+
}
|
|
277
289
|
// Set stdin to raw mode for Ink
|
|
278
290
|
if (process.stdin.isTTY) {
|
|
279
291
|
process.stdin.setRawMode(true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { existsSync, readdirSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
@@ -36,6 +36,18 @@ export function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSes
|
|
|
36
36
|
const [scrollOffset, setScrollOffset] = useState(0); // 0 = at bottom, positive = scrolled up
|
|
37
37
|
const [searchMode, setSearchMode] = useState(false);
|
|
38
38
|
const [searchQuery, setSearchQuery] = useState("");
|
|
39
|
+
const [expandedLogs, setExpandedLogs] = useState(false); // Start collapsed
|
|
40
|
+
const [terminalHeight, setTerminalHeight] = useState(process.stdout.rows ?? 30);
|
|
41
|
+
// Track terminal resize
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
const handleResize = () => {
|
|
44
|
+
setTerminalHeight(process.stdout.rows ?? 30);
|
|
45
|
+
};
|
|
46
|
+
process.stdout.on("resize", handleResize);
|
|
47
|
+
return () => {
|
|
48
|
+
process.stdout.off("resize", handleResize);
|
|
49
|
+
};
|
|
50
|
+
}, []);
|
|
39
51
|
// Use provided session start time or default to 1 second before component mount
|
|
40
52
|
const sessionStartTime = useMemo(() => {
|
|
41
53
|
if (providedSessionStartTime)
|
|
@@ -167,16 +179,6 @@ export function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSes
|
|
|
167
179
|
if (searchMode) {
|
|
168
180
|
return;
|
|
169
181
|
}
|
|
170
|
-
// Up arrow to scroll up
|
|
171
|
-
if (key.upArrow) {
|
|
172
|
-
setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, filteredLogs.length - 25)));
|
|
173
|
-
return;
|
|
174
|
-
}
|
|
175
|
-
// Down arrow to scroll down
|
|
176
|
-
if (key.downArrow) {
|
|
177
|
-
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
182
|
// 's' to cycle through service filters
|
|
181
183
|
if (input === "s") {
|
|
182
184
|
if (serviceFilter === null) {
|
|
@@ -213,26 +215,39 @@ export function LogsPane({ logsDir: customLogsDir, sessionStartTime: providedSes
|
|
|
213
215
|
setScrollOffset(0); // Also jump to bottom when clearing
|
|
214
216
|
return;
|
|
215
217
|
}
|
|
218
|
+
// 'e' to toggle expanded/collapsed metadata view
|
|
219
|
+
if (input === "e") {
|
|
220
|
+
setExpandedLogs((prev) => !prev);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
216
223
|
});
|
|
217
224
|
// Apply fuzzy search if active
|
|
218
225
|
const searchedLogs = searchQuery
|
|
219
226
|
? filteredLogs.filter((log) => fuzzyMatch(searchQuery, log.message))
|
|
220
227
|
: filteredLogs;
|
|
221
|
-
// Calculate display window
|
|
228
|
+
// Calculate display window based on terminal height
|
|
229
|
+
// Reserve space for: status line (3), header (3), search box if active (3), footer (3), padding (1)
|
|
230
|
+
const reservedLines = 10 + (searchMode ? 3 : 0);
|
|
231
|
+
const availableLines = Math.max(10, terminalHeight - reservedLines);
|
|
232
|
+
// When expanded, each log entry with metadata takes ~5 lines on average
|
|
233
|
+
// When collapsed, each log entry takes 1 line
|
|
234
|
+
const avgLinesPerEntry = expandedLogs ? 5 : 1;
|
|
235
|
+
const maxLines = Math.floor(availableLines / avgLinesPerEntry);
|
|
222
236
|
const totalLogs = searchedLogs.length;
|
|
223
|
-
const maxLines = 25;
|
|
224
237
|
const isAtBottom = scrollOffset === 0;
|
|
225
238
|
// Calculate which slice of logs to show
|
|
226
239
|
const displayLogs = isAtBottom
|
|
227
|
-
? searchedLogs.slice(-maxLines) // Show last
|
|
240
|
+
? searchedLogs.slice(-maxLines) // Show last N lines when at bottom
|
|
228
241
|
: searchedLogs.slice(Math.max(0, totalLogs - maxLines - scrollOffset), totalLogs - scrollOffset);
|
|
229
|
-
const
|
|
230
|
-
const
|
|
231
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%",
|
|
242
|
+
const _canScrollUp = totalLogs > maxLines && scrollOffset < totalLogs - maxLines;
|
|
243
|
+
const _canScrollDown = scrollOffset > 0;
|
|
244
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [searchMode && (_jsxs(Box, { borderStyle: "single", borderBottom: true, borderColor: "cyan", paddingX: 1, children: [_jsx(Text, { color: "cyan", children: "Search: " }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search..." }), _jsx(Text, { dimColor: true, children: " (ESC to exit)" })] })), _jsx(Box, { flexDirection: "column", paddingX: 1, flexGrow: 1, children: displayLogs.length === 0 ? (_jsx(Text, { dimColor: true, children: "No logs match the current filters. Press 'c' to clear filters." })) : (displayLogs.map((log, idx) => {
|
|
232
245
|
const serviceColor = SERVICE_COLORS[availableServices.indexOf(log.service) % SERVICE_COLORS.length] || "white";
|
|
233
246
|
const levelColor = LOG_COLORS[log.level] || "white";
|
|
234
247
|
const time = new Date(log.timestamp).toLocaleTimeString();
|
|
235
248
|
const keyStr = `${log.timestamp}-${idx}`;
|
|
236
|
-
return (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ["[", time, "]"] }), _jsxs(Text, { color: serviceColor, children: [" [", log.service, "]"] }), _jsxs(Text, { color: levelColor, children: [" [", log.level.toUpperCase(), "]"] }), _jsxs(Text, { children: [" ", log.message] })] },
|
|
237
|
-
|
|
249
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: ["[", time, "]"] }), _jsxs(Text, { color: serviceColor, children: [" [", log.service, "]"] }), _jsxs(Text, { color: levelColor, children: [" [", log.level.toUpperCase(), "]"] }), _jsxs(Text, { children: [" ", log.message] })] }), expandedLogs &&
|
|
250
|
+
log.metadata &&
|
|
251
|
+
Object.keys(log.metadata).length > 0 && (_jsx(Box, { paddingLeft: 2, children: _jsx(Text, { dimColor: true, children: JSON.stringify(log.metadata, null, 2) }) }))] }, keyStr));
|
|
252
|
+
})) }), _jsxs(Box, { borderStyle: "single", borderTop: true, borderColor: "gray", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [serviceFilter && _jsxs(Text, { children: ["[", serviceFilter, "] "] }), levelFilter && (_jsxs(Text, { children: ["[", ">=", levelFilter, "]", " "] })), searchQuery && _jsxs(Text, { color: "cyan", children: ["[SEARCH: ", searchQuery, "] "] }), !isAtBottom && _jsx(Text, { color: "yellow", children: "[SCROLLED] " }), expandedLogs && _jsx(Text, { color: "green", children: "[EXPANDED] " }), _jsxs(Text, { dimColor: true, children: [displayLogs.length, "/", totalLogs, " logs", scrollOffset > 0 && ` (${scrollOffset} from bottom)`] })] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "(/)search | (s)ervice | (l)evel | (e)xpand | (c)lear | ESC" }) })] })] }));
|
|
238
253
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text, useInput } from "ink";
|
|
3
3
|
import TextInput from "ink-text-input";
|
|
4
4
|
import { useEffect, useMemo, useState } from "react";
|
|
@@ -86,16 +86,6 @@ export function MergedLogsPane({ services, onClear }) {
|
|
|
86
86
|
if (searchMode) {
|
|
87
87
|
return;
|
|
88
88
|
}
|
|
89
|
-
// Up arrow to scroll up
|
|
90
|
-
if (key.upArrow) {
|
|
91
|
-
setScrollOffset((prev) => Math.min(prev + 1, Math.max(0, mergedLines.length - maxLines)));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
// Down arrow to scroll down
|
|
95
|
-
if (key.downArrow) {
|
|
96
|
-
setScrollOffset((prev) => Math.max(prev - 1, 0));
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
89
|
// 's' to cycle through service filters
|
|
100
90
|
if (input === "s") {
|
|
101
91
|
if (serviceFilter === null) {
|
|
@@ -170,9 +160,9 @@ export function MergedLogsPane({ services, onClear }) {
|
|
|
170
160
|
}
|
|
171
161
|
return searchedLines.slice(Math.max(0, searchedLines.length - maxLines - scrollOffset), searchedLines.length - scrollOffset);
|
|
172
162
|
}, [searchedLines, scrollOffset, isAtBottom]);
|
|
173
|
-
const
|
|
163
|
+
const _canScrollUp = searchedLines.length > maxLines &&
|
|
174
164
|
scrollOffset < searchedLines.length - maxLines;
|
|
175
|
-
const
|
|
165
|
+
const _canScrollDown = scrollOffset > 0;
|
|
176
166
|
// Get overall status (error if any error, stopped if all stopped, etc.)
|
|
177
167
|
const overallStatus = useMemo(() => {
|
|
178
168
|
if (services.some((s) => s.status === "error"))
|
|
@@ -190,12 +180,12 @@ export function MergedLogsPane({ services, onClear }) {
|
|
|
190
180
|
: overallStatus === "starting"
|
|
191
181
|
? "yellow"
|
|
192
182
|
: "gray";
|
|
193
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [
|
|
183
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [searchMode && (_jsxs(Box, { borderStyle: "single", borderBottom: true, borderColor: "cyan", paddingX: 1, marginBottom: 1, flexShrink: 0, children: [_jsx(Text, { color: "cyan", children: "Search: " }), _jsx(TextInput, { value: searchQuery, onChange: setSearchQuery, placeholder: "Type to search..." }), _jsx(Text, { dimColor: true, children: " (ESC to exit)" })] })), _jsx(Box, { flexDirection: "column", flexGrow: 1, minHeight: 0, paddingX: 1, children: displayLines.length === 0 ? (_jsx(Text, { dimColor: true, children: searchQuery || serviceFilter || levelFilter
|
|
194
184
|
? "No logs match the current filters. Press 'c' to clear, 's' for service, or 'l' for level."
|
|
195
185
|
: "Waiting for output..." })) : (displayLines.map((logLine, idx) => {
|
|
196
186
|
const serviceColor = SERVICE_COLORS[availableServices.indexOf(logLine.service) %
|
|
197
187
|
SERVICE_COLORS.length] || "white";
|
|
198
188
|
const keyStr = `${logLine.service}-${logLine.serviceIndex}-${logLine.index}-${idx}`;
|
|
199
189
|
return (_jsxs(Box, { children: [_jsxs(Text, { color: serviceColor, children: ["[", logLine.service, "]"] }), _jsxs(Text, { children: [" ", logLine.line] })] }, keyStr));
|
|
200
|
-
})) }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Text, {
|
|
190
|
+
})) }), _jsxs(Box, { borderStyle: "single", borderColor: "gray", paddingX: 1, flexShrink: 0, justifyContent: "space-between", children: [_jsxs(Box, { children: [_jsx(Text, { color: statusColor, children: "\u25CF" }), _jsxs(Text, { children: [" ", overallStatus] }), serviceFilter && _jsxs(Text, { children: [" | [", serviceFilter, "]"] }), levelFilter && (_jsxs(Text, { children: [" ", "| [", ">=", levelFilter, "]"] })), searchQuery && _jsxs(Text, { color: "cyan", children: [" | [SEARCH: ", searchQuery, "]"] }), !isAtBottom && _jsx(Text, { color: "yellow", children: " | [SCROLLED]" }), _jsxs(Text, { dimColor: true, children: [" ", "| ", displayLines.length, "/", searchedLines.length, " lines"] }), scrollOffset > 0 && (_jsxs(Text, { dimColor: true, children: [" (", scrollOffset, " from bottom)"] }))] }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "(/)search | (s)ervice | (l)evel | (c)lear | ESC" }) })] })] }));
|
|
201
191
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import type React from "react";
|
|
1
2
|
export interface StatusLineProps {
|
|
2
3
|
activeTab: number;
|
|
3
4
|
tabs: string[];
|
|
5
|
+
rightContent?: React.ReactNode;
|
|
4
6
|
}
|
|
5
|
-
export declare function StatusLine({ activeTab, tabs }: StatusLineProps): import("react/jsx-runtime").JSX.Element;
|
|
7
|
+
export declare function StatusLine({ activeTab, tabs, rightContent }: StatusLineProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
export function StatusLine({ activeTab, tabs }) {
|
|
4
|
-
return (_jsxs(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, children: [tabs.map((tab, idx) => (_jsxs(Text, { children: [_jsx(Text, { color: activeTab === idx ? "green" : "gray", children: tab }), idx < tabs.length - 1 && _jsx(Text, { color: "gray", children: " | " })] }, tab))), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: "yellow", children: "tab" }), _jsx(Text, { color: "gray", children: "
|
|
3
|
+
export function StatusLine({ activeTab, tabs, rightContent }) {
|
|
4
|
+
return (_jsxs(Box, { borderStyle: "single", borderColor: "blue", paddingX: 1, justifyContent: "space-between", children: [_jsxs(Box, { children: [tabs.map((tab, idx) => (_jsxs(Text, { children: [_jsx(Text, { color: activeTab === idx ? "green" : "gray", children: tab }), idx < tabs.length - 1 && _jsx(Text, { color: "gray", children: " | " })] }, tab))), _jsx(Text, { color: "gray", children: " | " }), _jsx(Text, { color: "yellow", children: "tab" }), _jsx(Text, { color: "gray", children: " to switch" })] }), rightContent && _jsx(Box, { children: rightContent })] }));
|
|
5
5
|
}
|
|
@@ -44,12 +44,6 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
|
|
|
44
44
|
setActiveTab((activeTab + 1) % allTabs.length);
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
if (key.leftArrow && activeTab > 0) {
|
|
48
|
-
setActiveTab(activeTab - 1);
|
|
49
|
-
}
|
|
50
|
-
else if (key.rightArrow && activeTab < allTabs.length - 1) {
|
|
51
|
-
setActiveTab(activeTab + 1);
|
|
52
|
-
}
|
|
53
47
|
});
|
|
54
48
|
// Set up process output listeners
|
|
55
49
|
useEffect(() => {
|
|
@@ -208,5 +202,9 @@ export function TabbedOutput({ processes, customTabs = [], logsDir, onExit, onPo
|
|
|
208
202
|
return (_jsx(Box, { flexDirection: "column", height: "100%", children: _jsx(MergedLogsPane, { services: serviceOutputs, onClear: handleClearMergedOutput }) }));
|
|
209
203
|
}
|
|
210
204
|
// TUI mode: has custom tabs - use tabbed interface
|
|
211
|
-
|
|
205
|
+
// Get status content from current tab if available
|
|
206
|
+
const statusContent = isCustomTab(currentTab) && currentTab.renderStatus
|
|
207
|
+
? currentTab.renderStatus()
|
|
208
|
+
: null;
|
|
209
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [isProcessTab(currentTab) ? (_jsx(ProcessPane, { title: currentTab.name, output: outputs[activeTab] || [], port: ports[activeTab], status: statuses[activeTab] || "starting", onClear: handleClearOutput })) : isCustomTab(currentTab) ? (currentTab.render()) : null, _jsx(StatusLine, { activeTab: activeTab, tabs: allTabs.map((t) => t.name), rightContent: statusContent })] }));
|
|
212
210
|
}
|
package/dist/index.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"town": "./dist/index.js"
|
|
@@ -9,25 +9,23 @@
|
|
|
9
9
|
"dist",
|
|
10
10
|
"README.md"
|
|
11
11
|
],
|
|
12
|
-
"repository":
|
|
13
|
-
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/federicoweber/agent_hub.git"
|
|
15
|
-
},
|
|
12
|
+
"repository": "github:townco/town",
|
|
16
13
|
"scripts": {
|
|
17
14
|
"check": "tsc --noEmit",
|
|
18
15
|
"build": "tsc"
|
|
19
16
|
},
|
|
20
17
|
"devDependencies": {
|
|
21
|
-
"@townco/tsconfig": "0.1.
|
|
18
|
+
"@townco/tsconfig": "0.1.21",
|
|
22
19
|
"@types/bun": "^1.3.1",
|
|
23
20
|
"@types/react": "^19.2.2"
|
|
24
21
|
},
|
|
25
22
|
"dependencies": {
|
|
26
23
|
"@optique/core": "^0.6.2",
|
|
27
24
|
"@optique/run": "^0.6.2",
|
|
28
|
-
"@townco/agent": "0.1.
|
|
29
|
-
"@townco/
|
|
30
|
-
"@townco/
|
|
25
|
+
"@townco/agent": "0.1.29",
|
|
26
|
+
"@townco/core": "0.0.2",
|
|
27
|
+
"@townco/secret": "0.1.24",
|
|
28
|
+
"@townco/ui": "0.1.24",
|
|
31
29
|
"@types/inquirer": "^9.0.9",
|
|
32
30
|
"ink": "^6.4.0",
|
|
33
31
|
"ink-text-input": "^6.0.0",
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
interface MCPAddProps {
|
|
2
|
-
name?: string;
|
|
3
|
-
url?: string;
|
|
4
|
-
command?: string;
|
|
5
|
-
args?: readonly string[];
|
|
6
|
-
}
|
|
7
|
-
declare function MCPAddApp({
|
|
8
|
-
name: initialName,
|
|
9
|
-
url: initialUrl,
|
|
10
|
-
command: initialCommand,
|
|
11
|
-
args: initialArgs,
|
|
12
|
-
}: MCPAddProps): import("react/jsx-runtime").JSX.Element | null;
|
|
13
|
-
export default MCPAddApp;
|
|
14
|
-
export declare function runMCPAdd(props?: MCPAddProps): Promise<void>;
|
package/dist/commands/mcp-add.js
DELETED
|
@@ -1,494 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { readFileSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { getAgentPath, listAgents } from "@townco/agent/storage";
|
|
5
|
-
import { MultiSelect, SingleSelect } from "@townco/ui/tui";
|
|
6
|
-
import { Box, render, Text, useApp, useInput } from "ink";
|
|
7
|
-
import TextInput from "ink-text-input";
|
|
8
|
-
import { useEffect, useState } from "react";
|
|
9
|
-
import { mcpConfigExists, saveMCPConfig } from "../lib/mcp-storage";
|
|
10
|
-
function TextInputStage({ title, value, onChange, onSubmit, onCancel, placeholder, }) {
|
|
11
|
-
useInput((_input, key) => {
|
|
12
|
-
if (key.escape) {
|
|
13
|
-
onCancel();
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: title }) }), _jsxs(Box, { children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: value, onChange: onChange, onSubmit: onSubmit, ...(placeholder && { placeholder }) })] }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Continue \u2022 Esc: Back" }) })] }));
|
|
17
|
-
}
|
|
18
|
-
function TransportSelectStage({ selected, onChange, onNext, onCancel, }) {
|
|
19
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Select transport type:" }) }), _jsx(SingleSelect, { options: [
|
|
20
|
-
{
|
|
21
|
-
label: "stdio",
|
|
22
|
-
value: "stdio",
|
|
23
|
-
description: "Run MCP server as a local process",
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
label: "HTTP",
|
|
27
|
-
value: "http",
|
|
28
|
-
description: "Connect to MCP server over HTTP",
|
|
29
|
-
},
|
|
30
|
-
], selected: selected, onChange: (transport) => onChange(transport), onSubmit: (transport) => onNext(transport), onCancel: onCancel })] }));
|
|
31
|
-
}
|
|
32
|
-
function NameInputStage({ value, onChange, onNext, onBack, onError, }) {
|
|
33
|
-
const handleSubmit = () => {
|
|
34
|
-
const trimmed = value.trim();
|
|
35
|
-
if (!trimmed) {
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
// Check for duplicate
|
|
39
|
-
if (mcpConfigExists(trimmed)) {
|
|
40
|
-
onError(`MCP server "${trimmed}" already exists`);
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
onNext(trimmed);
|
|
44
|
-
};
|
|
45
|
-
return (_jsx(TextInputStage, { title: "Enter MCP server name:", value: value, onChange: onChange, onSubmit: handleSubmit, onCancel: onBack, placeholder: "my-mcp-server" }));
|
|
46
|
-
}
|
|
47
|
-
function StdioCommandStage({ serverName, value, onChange, onNext, onBack, }) {
|
|
48
|
-
const handleSubmit = () => {
|
|
49
|
-
const trimmed = value.trim();
|
|
50
|
-
if (!trimmed) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
onNext(trimmed);
|
|
54
|
-
};
|
|
55
|
-
return (_jsx(TextInputStage, { title: `Enter command to run MCP server: ${serverName}`, value: value, onChange: onChange, onSubmit: handleSubmit, onCancel: onBack, placeholder: "npx @modelcontextprotocol/server-filesystem" }));
|
|
56
|
-
}
|
|
57
|
-
function StdioArgsStage({ serverName, args, onAddArg, onRemoveLastArg, onNext, onBack, }) {
|
|
58
|
-
const [argInput, setArgInput] = useState("");
|
|
59
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Add arguments for: ", serverName, " (", args.length, " added)"] }) }), args.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Current arguments:" }), args.map((arg, index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { children: [index + 1, ". ", arg] }) }, arg)))] })), _jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { children: [">", " "] }), _jsx(TextInput, { value: argInput, onChange: setArgInput, onSubmit: () => {
|
|
60
|
-
const trimmed = argInput.trim();
|
|
61
|
-
if (trimmed) {
|
|
62
|
-
onAddArg(trimmed);
|
|
63
|
-
setArgInput("");
|
|
64
|
-
}
|
|
65
|
-
}, placeholder: "Enter argument (or leave empty and press Enter to continue)" })] }), _jsx(SingleSelect, { options: [
|
|
66
|
-
{
|
|
67
|
-
label: "Done adding arguments",
|
|
68
|
-
value: "done",
|
|
69
|
-
description: "Continue to review",
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
label: "Remove last argument",
|
|
73
|
-
value: "remove",
|
|
74
|
-
description: args.length > 0
|
|
75
|
-
? `Remove: ${args[args.length - 1]}`
|
|
76
|
-
: "No arguments to remove",
|
|
77
|
-
},
|
|
78
|
-
], selected: null, onChange: () => { }, onSubmit: (value) => {
|
|
79
|
-
if (value === "done") {
|
|
80
|
-
onNext();
|
|
81
|
-
}
|
|
82
|
-
else if (value === "remove" && args.length > 0) {
|
|
83
|
-
onRemoveLastArg();
|
|
84
|
-
}
|
|
85
|
-
}, onCancel: onBack })] }));
|
|
86
|
-
}
|
|
87
|
-
function HttpUrlStage({ serverName, value, onChange, onNext, onBack, onError, }) {
|
|
88
|
-
const handleSubmit = () => {
|
|
89
|
-
const trimmed = value.trim();
|
|
90
|
-
if (!trimmed) {
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
// Basic URL validation
|
|
94
|
-
try {
|
|
95
|
-
new URL(trimmed);
|
|
96
|
-
}
|
|
97
|
-
catch {
|
|
98
|
-
onError("Invalid URL format");
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
onNext(trimmed);
|
|
102
|
-
};
|
|
103
|
-
return (_jsx(TextInputStage, { title: `Enter HTTP URL for MCP server: ${serverName}`, value: value, onChange: onChange, onSubmit: handleSubmit, onCancel: onBack, placeholder: "http://localhost:3000/mcp" }));
|
|
104
|
-
}
|
|
105
|
-
function HttpHeadersStage({ serverName, headers, onAddHeader, onNext, onBack, }) {
|
|
106
|
-
const [headerKeyInput, setHeaderKeyInput] = useState("");
|
|
107
|
-
const [headerValueInput, setHeaderValueInput] = useState("");
|
|
108
|
-
const [inputStage, setInputStage] = useState("key");
|
|
109
|
-
const headerEntries = Object.entries(headers);
|
|
110
|
-
useInput((_input, key) => {
|
|
111
|
-
if (key.escape && inputStage === "key") {
|
|
112
|
-
onBack();
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Add HTTP headers for: ", serverName, " (", headerEntries.length, " added)"] }) }), headerEntries.length > 0 && (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Current headers:" }), headerEntries.map(([key, value], index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { children: [index + 1, ". ", key, ": ", value] }) }, key)))] })), inputStage === "key" && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { children: [">", " Header name: "] }), _jsx(TextInput, { value: headerKeyInput, onChange: setHeaderKeyInput, onSubmit: () => {
|
|
116
|
-
const trimmed = headerKeyInput.trim();
|
|
117
|
-
if (trimmed) {
|
|
118
|
-
setInputStage("value");
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
onNext();
|
|
122
|
-
}
|
|
123
|
-
}, placeholder: "Enter header name (or leave empty to continue)" })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Submit header name or continue \u2022 Esc: Back" }) })] })), inputStage === "value" && (_jsxs(_Fragment, { children: [_jsxs(Box, { marginBottom: 1, children: [_jsxs(Text, { children: [">", " Value for ", headerKeyInput, ":", " "] }), _jsx(TextInput, { value: headerValueInput, onChange: setHeaderValueInput, onSubmit: () => {
|
|
124
|
-
const trimmedValue = headerValueInput.trim();
|
|
125
|
-
if (trimmedValue) {
|
|
126
|
-
onAddHeader(headerKeyInput, trimmedValue);
|
|
127
|
-
setHeaderKeyInput("");
|
|
128
|
-
setHeaderValueInput("");
|
|
129
|
-
setInputStage("key");
|
|
130
|
-
}
|
|
131
|
-
}, placeholder: "Header value" })] }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Enter: Add header" }) })] }))] }));
|
|
132
|
-
}
|
|
133
|
-
function ReviewStage({ config, onEdit, onSave, onBack }) {
|
|
134
|
-
const [reviewSelection, setReviewSelection] = useState(null);
|
|
135
|
-
const reviewOptions = [
|
|
136
|
-
{
|
|
137
|
-
label: "Save configuration",
|
|
138
|
-
value: "continue",
|
|
139
|
-
description: "Save and finish",
|
|
140
|
-
},
|
|
141
|
-
{
|
|
142
|
-
label: "Edit name",
|
|
143
|
-
value: "name",
|
|
144
|
-
description: "Change the MCP server name",
|
|
145
|
-
},
|
|
146
|
-
{
|
|
147
|
-
label: "Edit transport",
|
|
148
|
-
value: "transport",
|
|
149
|
-
description: "Change the transport type",
|
|
150
|
-
},
|
|
151
|
-
];
|
|
152
|
-
if (config.transport === "stdio") {
|
|
153
|
-
reviewOptions.push({
|
|
154
|
-
label: "Edit command",
|
|
155
|
-
value: "stdioCommand",
|
|
156
|
-
description: "Change the command",
|
|
157
|
-
}, {
|
|
158
|
-
label: "Edit arguments",
|
|
159
|
-
value: "stdioArgs",
|
|
160
|
-
description: "Change the arguments",
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
reviewOptions.push({
|
|
165
|
-
label: "Edit URL",
|
|
166
|
-
value: "httpUrl",
|
|
167
|
-
description: "Change the HTTP URL",
|
|
168
|
-
}, {
|
|
169
|
-
label: "Edit headers",
|
|
170
|
-
value: "httpHeaders",
|
|
171
|
-
description: "Change the HTTP headers",
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Review MCP Server Configuration:" }) }), _jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Name: " }), config.name] }), _jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Transport: " }), config.transport] }), config.transport === "stdio" && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "Command: " }), config.command] }), config.args && config.args.length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Arguments:" }), config.args.map((arg, index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: [index + 1, ". ", arg] }) }, arg)))] }))] })), config.transport === "http" && (_jsxs(_Fragment, { children: [_jsxs(Text, { children: [_jsx(Text, { bold: true, children: "URL: " }), config.url] }), config.headers && Object.keys(config.headers).length > 0 && (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Headers:" }), Object.entries(config.headers).map(([key, value], index) => (_jsx(Box, { paddingLeft: 2, children: _jsxs(Text, { dimColor: true, children: [index + 1, ". ", key, ": ", value] }) }, key)))] }))] }))] }), _jsx(SingleSelect, { options: reviewOptions, selected: reviewSelection, onChange: setReviewSelection, onSubmit: (value) => {
|
|
175
|
-
setReviewSelection(value);
|
|
176
|
-
if (value === "continue") {
|
|
177
|
-
onSave();
|
|
178
|
-
}
|
|
179
|
-
else if (value === "name" ||
|
|
180
|
-
value === "transport" ||
|
|
181
|
-
value === "stdioCommand" ||
|
|
182
|
-
value === "stdioArgs" ||
|
|
183
|
-
value === "httpUrl" ||
|
|
184
|
-
value === "httpHeaders") {
|
|
185
|
-
onEdit(value);
|
|
186
|
-
}
|
|
187
|
-
}, onCancel: onBack })] }));
|
|
188
|
-
}
|
|
189
|
-
function NoAgentsMessage({ onNext }) {
|
|
190
|
-
useEffect(() => {
|
|
191
|
-
const timer = setTimeout(() => {
|
|
192
|
-
onNext();
|
|
193
|
-
}, 1000);
|
|
194
|
-
return () => clearTimeout(timer);
|
|
195
|
-
}, [onNext]);
|
|
196
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { dimColor: true, children: "No agents found. MCP server will be saved globally." }), _jsx(Text, { dimColor: true, children: "You can attach it to agents later." })] }));
|
|
197
|
-
}
|
|
198
|
-
function AgentSelectionStage({ selectedAgents, onSelectedAgentsChange, onNext, onBack, }) {
|
|
199
|
-
const [availableAgents, setAvailableAgents] = useState([]);
|
|
200
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
201
|
-
useEffect(() => {
|
|
202
|
-
// Fetch available agents
|
|
203
|
-
const fetchAgents = async () => {
|
|
204
|
-
try {
|
|
205
|
-
const agents = await listAgents();
|
|
206
|
-
setAvailableAgents(agents);
|
|
207
|
-
}
|
|
208
|
-
catch (error) {
|
|
209
|
-
console.error("Error fetching agents:", error);
|
|
210
|
-
setAvailableAgents([]);
|
|
211
|
-
}
|
|
212
|
-
finally {
|
|
213
|
-
setIsLoading(false);
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
fetchAgents();
|
|
217
|
-
}, []);
|
|
218
|
-
// If still loading, show loading message
|
|
219
|
-
if (isLoading) {
|
|
220
|
-
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "Loading agents..." }) }));
|
|
221
|
-
}
|
|
222
|
-
// If no agents available, show info message and auto-proceed
|
|
223
|
-
if (availableAgents.length === 0) {
|
|
224
|
-
return _jsx(NoAgentsMessage, { onNext: onNext });
|
|
225
|
-
}
|
|
226
|
-
// Show agent selection UI
|
|
227
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Attach MCP server to agents (optional):" }) }), _jsxs(Box, { marginBottom: 1, children: [_jsx(Text, { dimColor: true, children: "Select which agents should have access to this MCP server." }), _jsx(Text, { dimColor: true, children: "You can skip this step and attach agents later." })] }), _jsx(MultiSelect, { options: availableAgents.map((agent) => ({
|
|
228
|
-
label: agent,
|
|
229
|
-
value: agent,
|
|
230
|
-
})), selected: selectedAgents, onChange: onSelectedAgentsChange, onSubmit: onNext, onCancel: onBack })] }));
|
|
231
|
-
}
|
|
232
|
-
function DoneStage({ config, status, error, attachedAgents }) {
|
|
233
|
-
const { exit } = useApp();
|
|
234
|
-
useEffect(() => {
|
|
235
|
-
if (status === "done") {
|
|
236
|
-
setImmediate(() => {
|
|
237
|
-
exit();
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
}, [status, exit]);
|
|
241
|
-
if (status === "saving") {
|
|
242
|
-
return (_jsx(Box, { flexDirection: "column", children: _jsx(Text, { children: "\u23F3 Saving MCP server configuration..." }) }));
|
|
243
|
-
}
|
|
244
|
-
if (status === "error") {
|
|
245
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "red", children: "\u274C Error saving MCP server" }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { children: error }) })] }));
|
|
246
|
-
}
|
|
247
|
-
if (status === "done") {
|
|
248
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: "green", children: "\u2705 MCP server saved successfully!" }), _jsx(Box, { marginTop: 1, children: _jsxs(Text, { dimColor: true, children: ["Name: ", config.name] }) }), attachedAgents.length > 0 && (_jsx(Box, { marginTop: 1, flexDirection: "column", children: _jsxs(Text, { dimColor: true, children: ["Attached to agents: ", attachedAgents.join(", ")] }) }))] }));
|
|
249
|
-
}
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
252
|
-
// ============================================================================
|
|
253
|
-
// Main Component
|
|
254
|
-
// ============================================================================
|
|
255
|
-
function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand, args: initialArgs, }) {
|
|
256
|
-
// Determine the starting stage based on what's provided
|
|
257
|
-
const determineInitialStage = () => {
|
|
258
|
-
// If command provided, transport is stdio
|
|
259
|
-
if (initialCommand) {
|
|
260
|
-
return initialName ? "stdioArgs" : "name";
|
|
261
|
-
}
|
|
262
|
-
// If URL provided, transport is http
|
|
263
|
-
if (initialUrl) {
|
|
264
|
-
return initialName ? "httpHeaders" : "name";
|
|
265
|
-
}
|
|
266
|
-
return "transport";
|
|
267
|
-
};
|
|
268
|
-
const { exit } = useApp();
|
|
269
|
-
const [stage, setStage] = useState(determineInitialStage());
|
|
270
|
-
const [config, setConfig] = useState({
|
|
271
|
-
...(initialName && { name: initialName }),
|
|
272
|
-
...(initialCommand && { transport: "stdio" }),
|
|
273
|
-
...(initialUrl && !initialCommand && { transport: "http" }),
|
|
274
|
-
...(initialCommand && { command: initialCommand }),
|
|
275
|
-
...(initialArgs && initialArgs.length > 0 && { args: [...initialArgs] }),
|
|
276
|
-
...(initialUrl && { url: initialUrl }),
|
|
277
|
-
});
|
|
278
|
-
const [nameInput, setNameInput] = useState(initialName || "");
|
|
279
|
-
const [commandInput, setCommandInput] = useState(initialCommand || "");
|
|
280
|
-
const [urlInput, setUrlInput] = useState(initialUrl || "");
|
|
281
|
-
const [isEditingFromReview, setIsEditingFromReview] = useState(false);
|
|
282
|
-
const [saveStatus, setSaveStatus] = useState("pending");
|
|
283
|
-
const [saveError, setSaveError] = useState(null);
|
|
284
|
-
const [selectedAgents, setSelectedAgents] = useState([]);
|
|
285
|
-
const handleSave = (agentsToAttach) => {
|
|
286
|
-
setSaveStatus("saving");
|
|
287
|
-
// Save the config
|
|
288
|
-
try {
|
|
289
|
-
if (!config.name || !config.transport) {
|
|
290
|
-
setSaveStatus("error");
|
|
291
|
-
setSaveError("Missing required fields");
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (config.transport === "stdio" &&
|
|
295
|
-
(!config.command || !config.command.trim())) {
|
|
296
|
-
setSaveStatus("error");
|
|
297
|
-
setSaveError("Command is required for stdio transport");
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
if (config.transport === "http" && (!config.url || !config.url.trim())) {
|
|
301
|
-
setSaveStatus("error");
|
|
302
|
-
setSaveError("URL is required for http transport");
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const mcpConfig = {
|
|
306
|
-
name: config.name,
|
|
307
|
-
transport: config.transport,
|
|
308
|
-
...(config.transport === "stdio" && {
|
|
309
|
-
command: config.command,
|
|
310
|
-
args: config.args,
|
|
311
|
-
}),
|
|
312
|
-
...(config.transport === "http" && {
|
|
313
|
-
url: config.url,
|
|
314
|
-
...(config.headers && { headers: config.headers }),
|
|
315
|
-
}),
|
|
316
|
-
};
|
|
317
|
-
// Save to global MCP storage
|
|
318
|
-
saveMCPConfig(mcpConfig);
|
|
319
|
-
// Update selected agents
|
|
320
|
-
for (const agentName of agentsToAttach) {
|
|
321
|
-
try {
|
|
322
|
-
const agentPath = getAgentPath(agentName);
|
|
323
|
-
const agentJsonPath = join(agentPath, "agent.json");
|
|
324
|
-
// Read existing agent.json
|
|
325
|
-
const agentJsonContent = readFileSync(agentJsonPath, "utf-8");
|
|
326
|
-
const agentDef = JSON.parse(agentJsonContent);
|
|
327
|
-
// Add MCP config to mcps array (create array if doesn't exist)
|
|
328
|
-
if (!agentDef.mcps) {
|
|
329
|
-
agentDef.mcps = [];
|
|
330
|
-
}
|
|
331
|
-
// Check if this MCP server is already in the agent's config
|
|
332
|
-
const existingIndex = agentDef.mcps.findIndex((mcp) => mcp.name === mcpConfig.name);
|
|
333
|
-
if (existingIndex >= 0) {
|
|
334
|
-
// Update existing config
|
|
335
|
-
agentDef.mcps[existingIndex] = mcpConfig;
|
|
336
|
-
}
|
|
337
|
-
else {
|
|
338
|
-
// Add new config
|
|
339
|
-
agentDef.mcps.push(mcpConfig);
|
|
340
|
-
}
|
|
341
|
-
// Write back to agent.json
|
|
342
|
-
writeFileSync(agentJsonPath, JSON.stringify(agentDef, null, 2));
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
console.error(`Error updating agent ${agentName}:`, error);
|
|
346
|
-
// Continue with other agents even if one fails
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
setSaveStatus("done");
|
|
350
|
-
}
|
|
351
|
-
catch (error) {
|
|
352
|
-
setSaveStatus("error");
|
|
353
|
-
setSaveError(error instanceof Error ? error.message : "Unknown error occurred");
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
// Transport selection stage
|
|
357
|
-
if (stage === "transport") {
|
|
358
|
-
return (_jsx(TransportSelectStage, { selected: config.transport || null, onChange: (transport) => setConfig({ ...config, transport }), onNext: (transport) => {
|
|
359
|
-
setConfig({ ...config, transport });
|
|
360
|
-
if (isEditingFromReview) {
|
|
361
|
-
setIsEditingFromReview(false);
|
|
362
|
-
setStage("review");
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
setStage("name");
|
|
366
|
-
}
|
|
367
|
-
}, onCancel: () => exit() }));
|
|
368
|
-
}
|
|
369
|
-
// Name input stage
|
|
370
|
-
if (stage === "name") {
|
|
371
|
-
return (_jsx(NameInputStage, { value: nameInput, onChange: setNameInput, onNext: (name) => {
|
|
372
|
-
setConfig({ ...config, name });
|
|
373
|
-
if (isEditingFromReview) {
|
|
374
|
-
setIsEditingFromReview(false);
|
|
375
|
-
setStage("review");
|
|
376
|
-
}
|
|
377
|
-
else if (config.transport === "stdio") {
|
|
378
|
-
setStage("stdioCommand");
|
|
379
|
-
}
|
|
380
|
-
else {
|
|
381
|
-
setStage("httpUrl");
|
|
382
|
-
}
|
|
383
|
-
}, onBack: () => {
|
|
384
|
-
if (initialCommand || initialUrl) {
|
|
385
|
-
exit();
|
|
386
|
-
}
|
|
387
|
-
else {
|
|
388
|
-
setStage("transport");
|
|
389
|
-
}
|
|
390
|
-
}, onError: setSaveError }));
|
|
391
|
-
}
|
|
392
|
-
// Stdio command input stage
|
|
393
|
-
if (stage === "stdioCommand") {
|
|
394
|
-
return (_jsx(StdioCommandStage, { serverName: config.name || "", value: commandInput, onChange: setCommandInput, onNext: (command) => {
|
|
395
|
-
setConfig({ ...config, command });
|
|
396
|
-
if (isEditingFromReview) {
|
|
397
|
-
setIsEditingFromReview(false);
|
|
398
|
-
setStage("review");
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
setStage("stdioArgs");
|
|
402
|
-
}
|
|
403
|
-
}, onBack: () => {
|
|
404
|
-
if (initialCommand) {
|
|
405
|
-
exit();
|
|
406
|
-
}
|
|
407
|
-
else {
|
|
408
|
-
setStage("name");
|
|
409
|
-
}
|
|
410
|
-
} }));
|
|
411
|
-
}
|
|
412
|
-
// Stdio args input stage
|
|
413
|
-
if (stage === "stdioArgs") {
|
|
414
|
-
return (_jsx(StdioArgsStage, { serverName: config.name || "", args: config.args || [], onAddArg: (arg) => setConfig({ ...config, args: [...(config.args || []), arg] }), onRemoveLastArg: () => setConfig({ ...config, args: (config.args || []).slice(0, -1) }), onNext: () => {
|
|
415
|
-
if (isEditingFromReview) {
|
|
416
|
-
setIsEditingFromReview(false);
|
|
417
|
-
}
|
|
418
|
-
setStage("review");
|
|
419
|
-
}, onBack: () => setStage("stdioCommand") }));
|
|
420
|
-
}
|
|
421
|
-
// HTTP URL input stage
|
|
422
|
-
if (stage === "httpUrl") {
|
|
423
|
-
return (_jsx(HttpUrlStage, { serverName: config.name || "", value: urlInput, onChange: setUrlInput, onNext: (url) => {
|
|
424
|
-
setConfig({ ...config, url });
|
|
425
|
-
if (isEditingFromReview) {
|
|
426
|
-
setIsEditingFromReview(false);
|
|
427
|
-
setStage("review");
|
|
428
|
-
}
|
|
429
|
-
else {
|
|
430
|
-
setStage("httpHeaders");
|
|
431
|
-
}
|
|
432
|
-
}, onBack: () => {
|
|
433
|
-
if (initialUrl) {
|
|
434
|
-
exit();
|
|
435
|
-
}
|
|
436
|
-
else {
|
|
437
|
-
setStage("name");
|
|
438
|
-
}
|
|
439
|
-
}, onError: setSaveError }));
|
|
440
|
-
}
|
|
441
|
-
// HTTP Headers input stage
|
|
442
|
-
if (stage === "httpHeaders") {
|
|
443
|
-
return (_jsx(HttpHeadersStage, { serverName: config.name || "", headers: config.headers || {}, onAddHeader: (key, value) => setConfig({
|
|
444
|
-
...config,
|
|
445
|
-
headers: { ...(config.headers || {}), [key]: value },
|
|
446
|
-
}), onNext: () => {
|
|
447
|
-
if (isEditingFromReview) {
|
|
448
|
-
setIsEditingFromReview(false);
|
|
449
|
-
}
|
|
450
|
-
setStage("review");
|
|
451
|
-
}, onBack: () => setStage("httpUrl") }));
|
|
452
|
-
}
|
|
453
|
-
// Review stage
|
|
454
|
-
if (stage === "review") {
|
|
455
|
-
return (_jsx(ReviewStage, { config: config, onEdit: (editStage) => {
|
|
456
|
-
setIsEditingFromReview(true);
|
|
457
|
-
setStage(editStage);
|
|
458
|
-
}, onSave: () => {
|
|
459
|
-
setStage("agentSelection");
|
|
460
|
-
}, onBack: () => {
|
|
461
|
-
if (config.transport === "stdio") {
|
|
462
|
-
setStage("stdioArgs");
|
|
463
|
-
}
|
|
464
|
-
else {
|
|
465
|
-
setStage("httpHeaders");
|
|
466
|
-
}
|
|
467
|
-
} }));
|
|
468
|
-
}
|
|
469
|
-
// Agent selection stage
|
|
470
|
-
if (stage === "agentSelection") {
|
|
471
|
-
return (_jsx(AgentSelectionStage, { selectedAgents: selectedAgents, onSelectedAgentsChange: setSelectedAgents, onNext: () => {
|
|
472
|
-
handleSave(selectedAgents);
|
|
473
|
-
setStage("done");
|
|
474
|
-
}, onBack: () => setStage("review") }));
|
|
475
|
-
}
|
|
476
|
-
// Done stage
|
|
477
|
-
if (stage === "done") {
|
|
478
|
-
return (_jsx(DoneStage, { config: config, status: saveStatus, error: saveError, attachedAgents: selectedAgents }));
|
|
479
|
-
}
|
|
480
|
-
return null;
|
|
481
|
-
}
|
|
482
|
-
// ============================================================================
|
|
483
|
-
// Export and Runner
|
|
484
|
-
// ============================================================================
|
|
485
|
-
export default MCPAddApp;
|
|
486
|
-
export async function runMCPAdd(props = {}) {
|
|
487
|
-
// Set stdin to raw mode to capture input
|
|
488
|
-
if (process.stdin.isTTY) {
|
|
489
|
-
process.stdin.setRawMode(true);
|
|
490
|
-
}
|
|
491
|
-
const { waitUntilExit } = render(_jsx(MCPAddApp, { ...props }));
|
|
492
|
-
// Wait for the app to exit before returning
|
|
493
|
-
await waitUntilExit();
|
|
494
|
-
}
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Box, render, Text, useApp, useInput } from "ink";
|
|
3
|
-
import { useEffect, useState } from "react";
|
|
4
|
-
import { listMCPConfigs } from "../lib/mcp-storage";
|
|
5
|
-
// ============================================================================
|
|
6
|
-
// Main Component
|
|
7
|
-
// ============================================================================
|
|
8
|
-
function MCPListApp() {
|
|
9
|
-
const [result, setResult] = useState(null);
|
|
10
|
-
const [loading, setLoading] = useState(true);
|
|
11
|
-
const { exit } = useApp();
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
function loadConfigs() {
|
|
14
|
-
try {
|
|
15
|
-
const configs = listMCPConfigs();
|
|
16
|
-
setResult({ configs });
|
|
17
|
-
}
|
|
18
|
-
catch (error) {
|
|
19
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
20
|
-
setResult({ configs: [], error: errorMsg });
|
|
21
|
-
}
|
|
22
|
-
finally {
|
|
23
|
-
setLoading(false);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
loadConfigs();
|
|
27
|
-
}, []);
|
|
28
|
-
// Exit on any key press when not loading
|
|
29
|
-
useInput((_input, key) => {
|
|
30
|
-
if (!loading) {
|
|
31
|
-
if (key.return || key.escape || _input === "q") {
|
|
32
|
-
exit();
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
if (loading) {
|
|
37
|
-
return (_jsx(Box, { children: _jsx(Text, { children: "Loading MCP servers..." }) }));
|
|
38
|
-
}
|
|
39
|
-
if (result?.error) {
|
|
40
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "red", children: "\u274C Error loading MCP servers" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { children: result.error }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Press Enter or Q to exit" }) })] }));
|
|
41
|
-
}
|
|
42
|
-
if (!result || result.configs.length === 0) {
|
|
43
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "No MCP servers configured" }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: "Add one with: town mcp add" }) }), _jsx(Box, { children: _jsx(Text, { dimColor: true, children: "Press Enter or Q to exit" }) })] }));
|
|
44
|
-
}
|
|
45
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsxs(Text, { bold: true, children: ["Configured MCP Servers (", result.configs.length, ")"] }) }), result.configs.map((config, index) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsx(Box, { children: _jsxs(Text, { bold: true, color: "cyan", children: [index + 1, ". ", config.name] }) }), _jsx(Box, { paddingLeft: 3, children: config.transport === "http" ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Transport: ", _jsx(Text, { color: "green", children: "HTTP" })] }), _jsxs(Text, { children: ["URL: ", config.url] }), config.headers && Object.keys(config.headers).length > 0 && (_jsxs(Text, { children: ["Headers:", " ", Object.entries(config.headers)
|
|
46
|
-
.map(([key, value]) => `${key}: ${value}`)
|
|
47
|
-
.join(", ")] }))] })) : (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { children: ["Transport: ", _jsx(Text, { color: "blue", children: "stdio" })] }), _jsxs(Text, { children: ["Command: ", config.command] }), config.args && config.args.length > 0 && (_jsxs(Text, { children: ["Args: ", config.args.join(" ")] }))] })) })] }, config.name))), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Use `town mcp remove` to remove a server or `town mcp add` to add one" }) }), _jsx(Box, { marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Enter or Q to exit" }) })] }));
|
|
48
|
-
}
|
|
49
|
-
// ============================================================================
|
|
50
|
-
// Export and Runner
|
|
51
|
-
// ============================================================================
|
|
52
|
-
export default MCPListApp;
|
|
53
|
-
export async function runMCPList() {
|
|
54
|
-
const { waitUntilExit, clear } = render(_jsx(MCPListApp, {}));
|
|
55
|
-
try {
|
|
56
|
-
await waitUntilExit();
|
|
57
|
-
}
|
|
58
|
-
finally {
|
|
59
|
-
clear();
|
|
60
|
-
// Ensure cursor is visible
|
|
61
|
-
process.stdout.write("\x1B[?25h");
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { SingleSelect } from "@townco/ui/tui";
|
|
3
|
-
import { Box, render, Text } from "ink";
|
|
4
|
-
import { useEffect, useState } from "react";
|
|
5
|
-
import { deleteMCPConfig, getMCPSummary, listMCPConfigs, } from "../lib/mcp-storage";
|
|
6
|
-
// ============================================================================
|
|
7
|
-
// Main Component
|
|
8
|
-
// ============================================================================
|
|
9
|
-
function MCPRemoveApp() {
|
|
10
|
-
const [stage, setStage] = useState("loading");
|
|
11
|
-
const [configs, setConfigs] = useState([]);
|
|
12
|
-
const [selectedName, setSelectedName] = useState(null);
|
|
13
|
-
const [selectedConfig, setSelectedConfig] = useState(null);
|
|
14
|
-
const [errorMessage, setErrorMessage] = useState("");
|
|
15
|
-
// Load configs on mount
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
function loadConfigs() {
|
|
18
|
-
try {
|
|
19
|
-
const configList = listMCPConfigs();
|
|
20
|
-
setConfigs(configList);
|
|
21
|
-
setStage(configList.length > 0 ? "select" : "error");
|
|
22
|
-
if (configList.length === 0) {
|
|
23
|
-
setErrorMessage("No MCP servers configured");
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
catch (error) {
|
|
27
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
28
|
-
setErrorMessage(errorMsg);
|
|
29
|
-
setStage("error");
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
loadConfigs();
|
|
33
|
-
}, []);
|
|
34
|
-
// Handle removal
|
|
35
|
-
const handleRemove = () => {
|
|
36
|
-
if (!selectedName)
|
|
37
|
-
return;
|
|
38
|
-
setStage("removing");
|
|
39
|
-
try {
|
|
40
|
-
const success = deleteMCPConfig(selectedName);
|
|
41
|
-
if (success) {
|
|
42
|
-
setStage("done");
|
|
43
|
-
// Exit immediately
|
|
44
|
-
process.exit(0);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
throw new Error("Failed to delete MCP config");
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
catch (error) {
|
|
51
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
52
|
-
setErrorMessage(errorMsg);
|
|
53
|
-
setStage("error");
|
|
54
|
-
}
|
|
55
|
-
};
|
|
56
|
-
if (stage === "loading") {
|
|
57
|
-
return (_jsx(Box, { children: _jsx(Text, { children: "Loading MCP servers..." }) }));
|
|
58
|
-
}
|
|
59
|
-
if (stage === "error") {
|
|
60
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "red", children: "\u274C Error" }) }), _jsx(Box, { children: _jsx(Text, { children: errorMessage }) })] }));
|
|
61
|
-
}
|
|
62
|
-
if (stage === "select") {
|
|
63
|
-
const options = configs.map((config) => ({
|
|
64
|
-
label: config.name,
|
|
65
|
-
value: config.name,
|
|
66
|
-
description: getMCPSummary(config),
|
|
67
|
-
}));
|
|
68
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Select MCP server to remove" }) }), _jsx(SingleSelect, { options: options, selected: selectedName, onChange: setSelectedName, onSubmit: (name) => {
|
|
69
|
-
const found = configs.find((c) => c.name === name);
|
|
70
|
-
if (found) {
|
|
71
|
-
setSelectedName(name);
|
|
72
|
-
setSelectedConfig(found);
|
|
73
|
-
setStage("confirm");
|
|
74
|
-
}
|
|
75
|
-
}, onCancel: () => process.exit(0) })] }));
|
|
76
|
-
}
|
|
77
|
-
if (stage === "confirm") {
|
|
78
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, children: "Confirm removal" }) }), _jsx(Box, { marginBottom: 1, children: _jsxs(Text, { children: ["Are you sure you want to remove:", " ", _jsx(Text, { bold: true, children: selectedConfig?.name })] }) }), _jsx(Box, { marginBottom: 1, children: _jsx(Text, { dimColor: true, children: getMCPSummary(selectedConfig) }) }), _jsx(SingleSelect, { options: [
|
|
79
|
-
{
|
|
80
|
-
label: "Yes, remove it",
|
|
81
|
-
value: "yes",
|
|
82
|
-
description: "Permanently delete this MCP server configuration",
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
label: "No, cancel",
|
|
86
|
-
value: "no",
|
|
87
|
-
description: "Go back to selection",
|
|
88
|
-
},
|
|
89
|
-
], selected: null, onChange: () => { }, onSubmit: (choice) => {
|
|
90
|
-
if (choice === "yes") {
|
|
91
|
-
handleRemove();
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
setStage("select");
|
|
95
|
-
}
|
|
96
|
-
}, onCancel: () => setStage("select") })] }));
|
|
97
|
-
}
|
|
98
|
-
if (stage === "removing") {
|
|
99
|
-
return (_jsx(Box, { children: _jsx(Text, { children: "\uD83D\uDDD1\uFE0F Removing MCP server..." }) }));
|
|
100
|
-
}
|
|
101
|
-
if (stage === "done") {
|
|
102
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginBottom: 1, children: _jsx(Text, { bold: true, color: "green", children: "\u2705 MCP server removed successfully" }) }), _jsx(Box, { children: _jsxs(Text, { children: ["Removed: ", _jsx(Text, { bold: true, children: selectedConfig?.name })] }) })] }));
|
|
103
|
-
}
|
|
104
|
-
return _jsxs(Text, { children: ["Unknown stage: ", stage] });
|
|
105
|
-
}
|
|
106
|
-
// ============================================================================
|
|
107
|
-
// Export and Runner
|
|
108
|
-
// ============================================================================
|
|
109
|
-
export default MCPRemoveApp;
|
|
110
|
-
export async function runMCPRemove() {
|
|
111
|
-
const { waitUntilExit, clear } = render(_jsx(MCPRemoveApp, {}));
|
|
112
|
-
try {
|
|
113
|
-
await waitUntilExit();
|
|
114
|
-
}
|
|
115
|
-
finally {
|
|
116
|
-
clear();
|
|
117
|
-
// Ensure cursor is visible
|
|
118
|
-
process.stdout.write("\x1B[?25h");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
export type MCPConfig = {
|
|
2
|
-
name: string;
|
|
3
|
-
transport: "stdio" | "http";
|
|
4
|
-
command?: string;
|
|
5
|
-
args?: string[];
|
|
6
|
-
url?: string;
|
|
7
|
-
headers?: Record<string, string>;
|
|
8
|
-
};
|
|
9
|
-
/**
|
|
10
|
-
* Save an MCP config to the store
|
|
11
|
-
*/
|
|
12
|
-
export declare function saveMCPConfig(config: MCPConfig): void;
|
|
13
|
-
/**
|
|
14
|
-
* Load an MCP config by name
|
|
15
|
-
*/
|
|
16
|
-
export declare function loadMCPConfig(name: string): MCPConfig | null;
|
|
17
|
-
/**
|
|
18
|
-
* Delete an MCP config by name
|
|
19
|
-
*/
|
|
20
|
-
export declare function deleteMCPConfig(name: string): boolean;
|
|
21
|
-
/**
|
|
22
|
-
* List all MCP configs
|
|
23
|
-
*/
|
|
24
|
-
export declare function listMCPConfigs(): MCPConfig[];
|
|
25
|
-
/**
|
|
26
|
-
* Check if an MCP config exists
|
|
27
|
-
*/
|
|
28
|
-
export declare function mcpConfigExists(name: string): boolean;
|
|
29
|
-
/**
|
|
30
|
-
* Get a summary of an MCP config for display
|
|
31
|
-
*/
|
|
32
|
-
export declare function getMCPSummary(config: MCPConfig): string;
|
package/dist/lib/mcp-storage.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
// ============================================================================
|
|
5
|
-
// Constants
|
|
6
|
-
// ============================================================================
|
|
7
|
-
const TOWN_CONFIG_DIR = join(homedir(), ".config", "town");
|
|
8
|
-
const MCPS_FILE = join(TOWN_CONFIG_DIR, "mcps.json");
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// Helper Functions
|
|
11
|
-
// ============================================================================
|
|
12
|
-
/**
|
|
13
|
-
* Ensure the config directory exists
|
|
14
|
-
*/
|
|
15
|
-
function ensureConfigDir() {
|
|
16
|
-
if (!existsSync(TOWN_CONFIG_DIR)) {
|
|
17
|
-
mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Load all MCP configs from the JSON file
|
|
22
|
-
*/
|
|
23
|
-
function loadStore() {
|
|
24
|
-
ensureConfigDir();
|
|
25
|
-
if (!existsSync(MCPS_FILE)) {
|
|
26
|
-
return {};
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
const content = readFileSync(MCPS_FILE, "utf-8");
|
|
30
|
-
return JSON.parse(content);
|
|
31
|
-
}
|
|
32
|
-
catch (error) {
|
|
33
|
-
throw new Error(`Failed to load MCP configs: ${error instanceof Error ? error.message : String(error)}`);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Save all MCP configs to the JSON file
|
|
38
|
-
*/
|
|
39
|
-
function saveStore(store) {
|
|
40
|
-
ensureConfigDir();
|
|
41
|
-
try {
|
|
42
|
-
const content = JSON.stringify(store, null, 2);
|
|
43
|
-
writeFileSync(MCPS_FILE, content, "utf-8");
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
throw new Error(`Failed to save MCP configs: ${error instanceof Error ? error.message : String(error)}`);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
// ============================================================================
|
|
50
|
-
// Public API
|
|
51
|
-
// ============================================================================
|
|
52
|
-
/**
|
|
53
|
-
* Save an MCP config to the store
|
|
54
|
-
*/
|
|
55
|
-
export function saveMCPConfig(config) {
|
|
56
|
-
const store = loadStore();
|
|
57
|
-
store[config.name] = config;
|
|
58
|
-
saveStore(store);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Load an MCP config by name
|
|
62
|
-
*/
|
|
63
|
-
export function loadMCPConfig(name) {
|
|
64
|
-
const store = loadStore();
|
|
65
|
-
return store[name] || null;
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* Delete an MCP config by name
|
|
69
|
-
*/
|
|
70
|
-
export function deleteMCPConfig(name) {
|
|
71
|
-
const store = loadStore();
|
|
72
|
-
if (store[name]) {
|
|
73
|
-
delete store[name];
|
|
74
|
-
saveStore(store);
|
|
75
|
-
return true;
|
|
76
|
-
}
|
|
77
|
-
return false;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* List all MCP configs
|
|
81
|
-
*/
|
|
82
|
-
export function listMCPConfigs() {
|
|
83
|
-
const store = loadStore();
|
|
84
|
-
return Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Check if an MCP config exists
|
|
88
|
-
*/
|
|
89
|
-
export function mcpConfigExists(name) {
|
|
90
|
-
const store = loadStore();
|
|
91
|
-
return name in store;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Get a summary of an MCP config for display
|
|
95
|
-
*/
|
|
96
|
-
export function getMCPSummary(config) {
|
|
97
|
-
if (config.transport === "http") {
|
|
98
|
-
const parts = [`HTTP: ${config.url}`];
|
|
99
|
-
if (config.headers && Object.keys(config.headers).length > 0) {
|
|
100
|
-
parts.push(`(${Object.keys(config.headers).length} header${Object.keys(config.headers).length === 1 ? "" : "s"})`);
|
|
101
|
-
}
|
|
102
|
-
return parts.join(" ");
|
|
103
|
-
}
|
|
104
|
-
else {
|
|
105
|
-
const parts = [`Stdio: ${config.command}`];
|
|
106
|
-
if (config.args && config.args.length > 0) {
|
|
107
|
-
parts.push(`[${config.args.join(" ")}]`);
|
|
108
|
-
}
|
|
109
|
-
return parts.join(" ");
|
|
110
|
-
}
|
|
111
|
-
}
|