-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcmd-mcp.ts
More file actions
206 lines (191 loc) · 8.01 KB
/
cmd-mcp.ts
File metadata and controls
206 lines (191 loc) · 8.01 KB
1
2
3
4
5
6
7
8
9
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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/**
* MCP command - Run MCP servers (stdio or HTTP transport)
*/
import { Command } from "commander";
import { FilesystemStore } from "../stores/filesystem.js";
import { runMCPServer } from "../clients/mcp-server.js";
import { parseIndexSpecs } from "../stores/index-spec.js";
import { CompositeStoreReader } from "../stores/composite.js";
import { ReadOnlyLayeredStore } from "../stores/read-only-layered-store.js";
// stdio subcommand (stdio-based MCP server for local clients like Claude Desktop)
const stdioCommand = new Command("stdio")
.description("Start MCP server using stdio transport (for Claude Desktop, etc.)")
.option(
"-i, --index <specs...>",
"Index spec(s): name, path:/path, or s3://bucket/key"
)
.option("--discovery", "Enable discovery mode (read-only, manage indexes via CLI)")
.option("--search-only", "Disable list_files/read_file tools (search only)")
.action(async (options) => {
try {
const indexSpecs: string[] | undefined = options.index;
const discoveryFlag = options.discovery;
let store;
let indexNames: string[] | undefined;
let discovery: boolean;
if (discoveryFlag && indexSpecs && indexSpecs.length > 0) {
// Discovery mode WITH remote indexes: merge local + remote
const specs = parseIndexSpecs(indexSpecs);
const remoteStore = await CompositeStoreReader.fromSpecs(specs);
const localStore = new FilesystemStore();
store = new ReadOnlyLayeredStore(localStore, remoteStore);
indexNames = undefined; // Discovery mode: no fixed list
discovery = true;
} else if (discoveryFlag) {
// Discovery mode with local indexes only
store = new FilesystemStore();
indexNames = undefined; // Discovery mode: no fixed list
discovery = true;
} else if (indexSpecs && indexSpecs.length > 0) {
// Fixed mode: use read-only CompositeStoreReader
const specs = parseIndexSpecs(indexSpecs);
indexNames = specs.map((s) => s.displayName);
store = await CompositeStoreReader.fromSpecs(specs);
discovery = false;
} else {
// No flags: restore original behavior - fail fast if no indexes
store = new FilesystemStore();
const availableIndexes = await store.list();
if (availableIndexes.length === 0) {
console.error("Error: No indexes found.");
console.error("The MCP server requires at least one index to operate.");
console.error("Run 'ctxc index --help' to see how to create an index.");
process.exit(1);
}
indexNames = availableIndexes;
discovery = false;
}
// Start MCP server (writes to stdout, reads from stdin)
await runMCPServer({
store,
indexNames,
searchOnly: options.searchOnly,
discovery,
});
} catch (error) {
// Write errors to stderr (stdout is for MCP protocol)
console.error("MCP server failed:", error);
process.exit(1);
}
});
// http subcommand (HTTP-based MCP server for remote clients)
const httpCommand = new Command("http")
.description("Start MCP server using Streamable HTTP transport")
.option(
"-i, --index <specs...>",
"Index spec(s): name, path:/path, or s3://bucket/key"
)
.option("--discovery", "Enable discovery mode (read-only, manage indexes via CLI)")
.option("--port <number>", "Port to listen on", "3000")
.option("--host <host>", "Host to bind to", "localhost")
.option("--cors <origins>", "CORS origins (comma-separated, or '*' for any)")
.option("--base-path <path>", "Base path for MCP endpoint", "/mcp")
.option("--search-only", "Disable list_files/read_file tools (search only)")
.option(
"--api-key <key>",
"API key for authentication (or set MCP_API_KEY env var)"
)
.action(async (options) => {
try {
const indexSpecs: string[] | undefined = options.index;
const discoveryFlag = options.discovery;
let store;
let indexNames: string[] | undefined;
let discovery: boolean;
if (discoveryFlag && indexSpecs && indexSpecs.length > 0) {
// Discovery mode WITH remote indexes: merge local + remote
const specs = parseIndexSpecs(indexSpecs);
const remoteStore = await CompositeStoreReader.fromSpecs(specs);
const localStore = new FilesystemStore();
store = new ReadOnlyLayeredStore(localStore, remoteStore);
indexNames = undefined; // Discovery mode: no fixed list
discovery = true;
} else if (discoveryFlag) {
// Discovery mode with local indexes only
store = new FilesystemStore();
indexNames = undefined; // Discovery mode: no fixed list
discovery = true;
} else if (indexSpecs && indexSpecs.length > 0) {
// Fixed mode: use read-only CompositeStoreReader
const specs = parseIndexSpecs(indexSpecs);
indexNames = specs.map((s) => s.displayName);
store = await CompositeStoreReader.fromSpecs(specs);
discovery = false;
} else {
// No flags: restore original behavior - fail fast if no indexes
store = new FilesystemStore();
const availableIndexes = await store.list();
if (availableIndexes.length === 0) {
console.error("Error: No indexes found.");
console.error("The MCP server requires at least one index to operate.");
console.error("Run 'ctxc index --help' to see how to create an index.");
process.exit(1);
}
indexNames = availableIndexes;
discovery = false;
}
// Parse CORS option
let cors: string | string[] | undefined;
if (options.cors) {
cors =
options.cors === "*"
? "*"
: options.cors.split(",").map((s: string) => s.trim());
}
// Get API key from option or environment
const apiKey = options.apiKey ?? process.env.MCP_API_KEY;
// Start HTTP server
const { runMCPHttpServer } = await import("../clients/mcp-http-server.js");
const server = await runMCPHttpServer({
store,
indexNames,
searchOnly: options.searchOnly,
discovery,
port: parseInt(options.port, 10),
host: options.host,
cors,
basePath: options.basePath,
apiKey,
});
console.log(`MCP HTTP server listening at ${server.getUrl()}`);
console.log(`Connect with MCP clients using Streamable HTTP transport`);
if (apiKey) {
console.log(`Authentication: API key required (Authorization: Bearer <key>)`);
} else {
console.log(`Authentication: None (open access)`);
}
// Security warnings for non-localhost bindings
const host = options.host;
const isLocalhost = host === "localhost" || host === "127.0.0.1" || host === "::1";
if (!isLocalhost) {
console.log();
console.log("⚠️ SECURITY WARNING: Server is binding to a non-localhost interface.");
console.log(" This server uses HTTP (not HTTPS) - all traffic is unencrypted.");
if (apiKey) {
console.log(" API keys will be transmitted in cleartext over the network.");
}
console.log();
console.log(" For production deployments, use one of these approaches:");
console.log(" • Place behind a TLS-terminating reverse proxy (nginx, Caddy, etc.)");
console.log(" • Use within a private network or VPN");
console.log(" • Bind to localhost and use SSH tunneling for remote access");
console.log();
}
// Handle shutdown
const shutdown = async () => {
console.log("\nShutting down...");
await server.stop();
process.exit(0);
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
} catch (error) {
console.error("Failed to start MCP HTTP server:", error);
process.exit(1);
}
});
// Main mcp command
export const mcpCommand = new Command("mcp")
.description("Run MCP servers (stdio or http transport)")
.addCommand(stdioCommand)
.addCommand(httpCommand);