@townco/cli 0.1.14 → 0.1.16
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/mcp-add.js +54 -5
- package/dist/commands/mcp-list.js +49 -174
- package/dist/commands/run.js +3 -5
- package/dist/lib/mcp-storage.d.ts +6 -5
- package/dist/lib/mcp-storage.js +52 -50
- package/package.json +5 -5
package/dist/commands/mcp-add.js
CHANGED
|
@@ -102,6 +102,34 @@ function HttpUrlStage({ serverName, value, onChange, onNext, onBack, onError, })
|
|
|
102
102
|
};
|
|
103
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
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
|
+
}
|
|
105
133
|
function ReviewStage({ config, onEdit, onSave, onBack }) {
|
|
106
134
|
const [reviewSelection, setReviewSelection] = useState(null);
|
|
107
135
|
const reviewOptions = [
|
|
@@ -137,9 +165,13 @@ function ReviewStage({ config, onEdit, onSave, onBack }) {
|
|
|
137
165
|
label: "Edit URL",
|
|
138
166
|
value: "httpUrl",
|
|
139
167
|
description: "Change the HTTP URL",
|
|
168
|
+
}, {
|
|
169
|
+
label: "Edit headers",
|
|
170
|
+
value: "httpHeaders",
|
|
171
|
+
description: "Change the HTTP headers",
|
|
140
172
|
});
|
|
141
173
|
}
|
|
142
|
-
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(Text, { children: [_jsx(Text, { bold: true, children: "URL: " }), config.url] }))] }), _jsx(SingleSelect, { options: reviewOptions, selected: reviewSelection, onChange: setReviewSelection, onSubmit: (value) => {
|
|
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) => {
|
|
143
175
|
setReviewSelection(value);
|
|
144
176
|
if (value === "continue") {
|
|
145
177
|
onSave();
|
|
@@ -148,7 +180,8 @@ function ReviewStage({ config, onEdit, onSave, onBack }) {
|
|
|
148
180
|
value === "transport" ||
|
|
149
181
|
value === "stdioCommand" ||
|
|
150
182
|
value === "stdioArgs" ||
|
|
151
|
-
value === "httpUrl"
|
|
183
|
+
value === "httpUrl" ||
|
|
184
|
+
value === "httpHeaders") {
|
|
152
185
|
onEdit(value);
|
|
153
186
|
}
|
|
154
187
|
}, onCancel: onBack })] }));
|
|
@@ -228,7 +261,7 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
228
261
|
}
|
|
229
262
|
// If URL provided, transport is http
|
|
230
263
|
if (initialUrl) {
|
|
231
|
-
return initialName ? "
|
|
264
|
+
return initialName ? "httpHeaders" : "name";
|
|
232
265
|
}
|
|
233
266
|
return "transport";
|
|
234
267
|
};
|
|
@@ -278,6 +311,7 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
278
311
|
}),
|
|
279
312
|
...(config.transport === "http" && {
|
|
280
313
|
url: config.url,
|
|
314
|
+
...(config.headers && { headers: config.headers }),
|
|
281
315
|
}),
|
|
282
316
|
};
|
|
283
317
|
// Save to global MCP storage
|
|
@@ -390,8 +424,11 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
390
424
|
setConfig({ ...config, url });
|
|
391
425
|
if (isEditingFromReview) {
|
|
392
426
|
setIsEditingFromReview(false);
|
|
427
|
+
setStage("review");
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
setStage("httpHeaders");
|
|
393
431
|
}
|
|
394
|
-
setStage("review");
|
|
395
432
|
}, onBack: () => {
|
|
396
433
|
if (initialUrl) {
|
|
397
434
|
exit();
|
|
@@ -401,6 +438,18 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
401
438
|
}
|
|
402
439
|
}, onError: setSaveError }));
|
|
403
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
|
+
}
|
|
404
453
|
// Review stage
|
|
405
454
|
if (stage === "review") {
|
|
406
455
|
return (_jsx(ReviewStage, { config: config, onEdit: (editStage) => {
|
|
@@ -413,7 +462,7 @@ function MCPAddApp({ name: initialName, url: initialUrl, command: initialCommand
|
|
|
413
462
|
setStage("stdioArgs");
|
|
414
463
|
}
|
|
415
464
|
else {
|
|
416
|
-
setStage("
|
|
465
|
+
setStage("httpHeaders");
|
|
417
466
|
}
|
|
418
467
|
} }));
|
|
419
468
|
}
|
|
@@ -1,188 +1,63 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
2
|
import { Box, render, Text, useApp, useInput } from "ink";
|
|
2
3
|
import { useEffect, useState } from "react";
|
|
3
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { listMCPConfigs } from "../lib/mcp-storage";
|
|
5
|
-
|
|
6
5
|
// ============================================================================
|
|
7
6
|
// Main Component
|
|
8
7
|
// ============================================================================
|
|
9
8
|
function MCPListApp() {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
children: "\u274C Error loading MCP servers",
|
|
50
|
-
}),
|
|
51
|
-
}),
|
|
52
|
-
_jsx(Box, {
|
|
53
|
-
marginBottom: 1,
|
|
54
|
-
children: _jsx(Text, { children: result.error }),
|
|
55
|
-
}),
|
|
56
|
-
_jsx(Box, {
|
|
57
|
-
children: _jsx(Text, {
|
|
58
|
-
dimColor: true,
|
|
59
|
-
children: "Press Enter or Q to exit",
|
|
60
|
-
}),
|
|
61
|
-
}),
|
|
62
|
-
],
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
if (!result || result.configs.length === 0) {
|
|
66
|
-
return _jsxs(Box, {
|
|
67
|
-
flexDirection: "column",
|
|
68
|
-
children: [
|
|
69
|
-
_jsx(Box, {
|
|
70
|
-
marginBottom: 1,
|
|
71
|
-
children: _jsx(Text, {
|
|
72
|
-
bold: true,
|
|
73
|
-
children: "No MCP servers configured",
|
|
74
|
-
}),
|
|
75
|
-
}),
|
|
76
|
-
_jsx(Box, {
|
|
77
|
-
marginBottom: 1,
|
|
78
|
-
children: _jsx(Text, {
|
|
79
|
-
dimColor: true,
|
|
80
|
-
children: "Add one with: town mcp add",
|
|
81
|
-
}),
|
|
82
|
-
}),
|
|
83
|
-
_jsx(Box, {
|
|
84
|
-
children: _jsx(Text, {
|
|
85
|
-
dimColor: true,
|
|
86
|
-
children: "Press Enter or Q to exit",
|
|
87
|
-
}),
|
|
88
|
-
}),
|
|
89
|
-
],
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
return _jsxs(Box, {
|
|
93
|
-
flexDirection: "column",
|
|
94
|
-
children: [
|
|
95
|
-
_jsx(Box, {
|
|
96
|
-
marginBottom: 1,
|
|
97
|
-
children: _jsxs(Text, {
|
|
98
|
-
bold: true,
|
|
99
|
-
children: ["Configured MCP Servers (", result.configs.length, ")"],
|
|
100
|
-
}),
|
|
101
|
-
}),
|
|
102
|
-
result.configs.map((config, index) =>
|
|
103
|
-
_jsxs(
|
|
104
|
-
Box,
|
|
105
|
-
{
|
|
106
|
-
flexDirection: "column",
|
|
107
|
-
marginBottom: 1,
|
|
108
|
-
children: [
|
|
109
|
-
_jsx(Box, {
|
|
110
|
-
children: _jsxs(Text, {
|
|
111
|
-
bold: true,
|
|
112
|
-
color: "cyan",
|
|
113
|
-
children: [index + 1, ". ", config.name],
|
|
114
|
-
}),
|
|
115
|
-
}),
|
|
116
|
-
_jsx(Box, {
|
|
117
|
-
paddingLeft: 3,
|
|
118
|
-
children:
|
|
119
|
-
config.transport === "http"
|
|
120
|
-
? _jsxs(Box, {
|
|
121
|
-
flexDirection: "column",
|
|
122
|
-
children: [
|
|
123
|
-
_jsxs(Text, {
|
|
124
|
-
children: [
|
|
125
|
-
"Transport: ",
|
|
126
|
-
_jsx(Text, { color: "green", children: "HTTP" }),
|
|
127
|
-
],
|
|
128
|
-
}),
|
|
129
|
-
_jsxs(Text, { children: ["URL: ", config.url] }),
|
|
130
|
-
],
|
|
131
|
-
})
|
|
132
|
-
: _jsxs(Box, {
|
|
133
|
-
flexDirection: "column",
|
|
134
|
-
children: [
|
|
135
|
-
_jsxs(Text, {
|
|
136
|
-
children: [
|
|
137
|
-
"Transport: ",
|
|
138
|
-
_jsx(Text, { color: "blue", children: "stdio" }),
|
|
139
|
-
],
|
|
140
|
-
}),
|
|
141
|
-
_jsxs(Text, {
|
|
142
|
-
children: ["Command: ", config.command],
|
|
143
|
-
}),
|
|
144
|
-
config.args &&
|
|
145
|
-
config.args.length > 0 &&
|
|
146
|
-
_jsxs(Text, {
|
|
147
|
-
children: ["Args: ", config.args.join(" ")],
|
|
148
|
-
}),
|
|
149
|
-
],
|
|
150
|
-
}),
|
|
151
|
-
}),
|
|
152
|
-
],
|
|
153
|
-
},
|
|
154
|
-
config.name,
|
|
155
|
-
),
|
|
156
|
-
),
|
|
157
|
-
_jsx(Box, {
|
|
158
|
-
marginTop: 1,
|
|
159
|
-
children: _jsx(Text, {
|
|
160
|
-
dimColor: true,
|
|
161
|
-
children:
|
|
162
|
-
"Use `town mcp remove` to remove a server or `town mcp add` to add one",
|
|
163
|
-
}),
|
|
164
|
-
}),
|
|
165
|
-
_jsx(Box, {
|
|
166
|
-
marginTop: 1,
|
|
167
|
-
children: _jsx(Text, {
|
|
168
|
-
dimColor: true,
|
|
169
|
-
children: "Press Enter or Q to exit",
|
|
170
|
-
}),
|
|
171
|
-
}),
|
|
172
|
-
],
|
|
173
|
-
});
|
|
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" }) })] }));
|
|
174
48
|
}
|
|
175
49
|
// ============================================================================
|
|
176
50
|
// Export and Runner
|
|
177
51
|
// ============================================================================
|
|
178
52
|
export default MCPListApp;
|
|
179
53
|
export async function runMCPList() {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
}
|
|
188
63
|
}
|
package/dist/commands/run.js
CHANGED
|
@@ -7,16 +7,14 @@ import { join } from "node:path";
|
|
|
7
7
|
import { agentExists, getAgentPath } from "@townco/agent/storage";
|
|
8
8
|
import { render } from "ink";
|
|
9
9
|
import open from "open";
|
|
10
|
-
import { useCallback, useMemo, useRef
|
|
10
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
11
11
|
import { TabbedOutput } from "../components/TabbedOutput.js";
|
|
12
12
|
import { findAvailablePort } from "../lib/port-utils.js";
|
|
13
13
|
function GuiRunner({ agentProcess, guiProcess, agentPort, onExit, }) {
|
|
14
|
-
const [guiPort, setGuiPort] = useState(5173);
|
|
15
14
|
const browserOpenedRef = useRef(false);
|
|
16
15
|
const handlePortDetected = useCallback((processIndex, port) => {
|
|
17
16
|
// Process index 1 is the GUI process
|
|
18
17
|
if (processIndex === 1) {
|
|
19
|
-
setGuiPort(port);
|
|
20
18
|
// Open browser once we know the actual port
|
|
21
19
|
if (!browserOpenedRef.current) {
|
|
22
20
|
browserOpenedRef.current = true;
|
|
@@ -33,8 +31,8 @@ function GuiRunner({ agentProcess, guiProcess, agentPort, onExit, }) {
|
|
|
33
31
|
// TabbedOutput will update the displayed port internally via onPortDetected callback
|
|
34
32
|
const processes = useMemo(() => [
|
|
35
33
|
{ name: "Agent", process: agentProcess, port: agentPort },
|
|
36
|
-
{ name: "GUI", process: guiProcess,
|
|
37
|
-
], [agentProcess, guiProcess, agentPort
|
|
34
|
+
{ name: "GUI", process: guiProcess }, // Port will be detected dynamically
|
|
35
|
+
], [agentProcess, guiProcess, agentPort]);
|
|
38
36
|
return (_jsx(TabbedOutput, { processes: processes, onExit: onExit, onPortDetected: handlePortDetected }));
|
|
39
37
|
}
|
|
40
38
|
async function loadEnvVars() {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
export type MCPConfig = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
name: string;
|
|
3
|
+
transport: "stdio" | "http";
|
|
4
|
+
command?: string;
|
|
5
|
+
args?: string[];
|
|
6
|
+
url?: string;
|
|
7
|
+
headers?: Record<string, string>;
|
|
7
8
|
};
|
|
8
9
|
/**
|
|
9
10
|
* Save an MCP config to the store
|
package/dist/lib/mcp-storage.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
|
|
5
4
|
// ============================================================================
|
|
6
5
|
// Constants
|
|
7
6
|
// ============================================================================
|
|
@@ -14,40 +13,38 @@ const MCPS_FILE = join(TOWN_CONFIG_DIR, "mcps.json");
|
|
|
14
13
|
* Ensure the config directory exists
|
|
15
14
|
*/
|
|
16
15
|
function ensureConfigDir() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
if (!existsSync(TOWN_CONFIG_DIR)) {
|
|
17
|
+
mkdirSync(TOWN_CONFIG_DIR, { recursive: true });
|
|
18
|
+
}
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
21
|
* Load all MCP configs from the JSON file
|
|
23
22
|
*/
|
|
24
23
|
function loadStore() {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
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
|
+
}
|
|
37
35
|
}
|
|
38
36
|
/**
|
|
39
37
|
* Save all MCP configs to the JSON file
|
|
40
38
|
*/
|
|
41
39
|
function saveStore(store) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
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
|
+
}
|
|
51
48
|
}
|
|
52
49
|
// ============================================================================
|
|
53
50
|
// Public API
|
|
@@ -56,54 +53,59 @@ function saveStore(store) {
|
|
|
56
53
|
* Save an MCP config to the store
|
|
57
54
|
*/
|
|
58
55
|
export function saveMCPConfig(config) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
const store = loadStore();
|
|
57
|
+
store[config.name] = config;
|
|
58
|
+
saveStore(store);
|
|
62
59
|
}
|
|
63
60
|
/**
|
|
64
61
|
* Load an MCP config by name
|
|
65
62
|
*/
|
|
66
63
|
export function loadMCPConfig(name) {
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
const store = loadStore();
|
|
65
|
+
return store[name] || null;
|
|
69
66
|
}
|
|
70
67
|
/**
|
|
71
68
|
* Delete an MCP config by name
|
|
72
69
|
*/
|
|
73
70
|
export function deleteMCPConfig(name) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
71
|
+
const store = loadStore();
|
|
72
|
+
if (store[name]) {
|
|
73
|
+
delete store[name];
|
|
74
|
+
saveStore(store);
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
81
78
|
}
|
|
82
79
|
/**
|
|
83
80
|
* List all MCP configs
|
|
84
81
|
*/
|
|
85
82
|
export function listMCPConfigs() {
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
const store = loadStore();
|
|
84
|
+
return Object.values(store).sort((a, b) => a.name.localeCompare(b.name));
|
|
88
85
|
}
|
|
89
86
|
/**
|
|
90
87
|
* Check if an MCP config exists
|
|
91
88
|
*/
|
|
92
89
|
export function mcpConfigExists(name) {
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
const store = loadStore();
|
|
91
|
+
return name in store;
|
|
95
92
|
}
|
|
96
93
|
/**
|
|
97
94
|
* Get a summary of an MCP config for display
|
|
98
95
|
*/
|
|
99
96
|
export function getMCPSummary(config) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
}
|
|
109
111
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@townco/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"town": "./dist/index.js"
|
|
@@ -19,16 +19,16 @@
|
|
|
19
19
|
"build": "tsc"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@townco/tsconfig": "0.1.
|
|
22
|
+
"@townco/tsconfig": "0.1.8",
|
|
23
23
|
"@types/bun": "^1.3.1",
|
|
24
24
|
"@types/react": "^19.2.2"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"@optique/core": "^0.6.2",
|
|
28
28
|
"@optique/run": "^0.6.2",
|
|
29
|
-
"@townco/agent": "0.1.
|
|
30
|
-
"@townco/secret": "0.1.
|
|
31
|
-
"@townco/ui": "0.1.
|
|
29
|
+
"@townco/agent": "0.1.16",
|
|
30
|
+
"@townco/secret": "0.1.11",
|
|
31
|
+
"@townco/ui": "0.1.11",
|
|
32
32
|
"@types/inquirer": "^9.0.9",
|
|
33
33
|
"ink": "^6.4.0",
|
|
34
34
|
"ink-text-input": "^6.0.0",
|