easy-local-mcp 0.3.9
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/LICENSE +21 -0
- package/README.md +417 -0
- package/chatgpt_plugin.png +0 -0
- package/chatgpt_setting.png +0 -0
- package/dist/agent.js +498 -0
- package/dist/command.js +30 -0
- package/dist/config-watch.js +35 -0
- package/dist/config.js +83 -0
- package/dist/control-endpoint.js +13 -0
- package/dist/control-ui.js +1025 -0
- package/dist/desktop.js +133 -0
- package/dist/index.js +339 -0
- package/dist/lifecycle.js +321 -0
- package/dist/mcp/loader.js +86 -0
- package/dist/process.js +122 -0
- package/dist/relay-config.js +145 -0
- package/dist/relay-protocol.js +34 -0
- package/dist/relay.js +16 -0
- package/dist/security.js +238 -0
- package/dist/server.js +253 -0
- package/dist/skills/loader.js +24 -0
- package/dist/tray.js +74 -0
- package/dist/workspace.js +282 -0
- package/easy-local-mcp.png +0 -0
- package/easy-local-mcp.svg +56 -0
- package/localmcp.example.json +21 -0
- package/package.json +90 -0
- package/scripts/prepare-desktop-bundle.mjs +81 -0
- package/scripts/run-cargo.mjs +35 -0
- package/scripts/run-tauri.mjs +33 -0
- package/scripts/worker-setup.mjs +20 -0
- package/skills/computer-use/SKILL.md +20 -0
- package/skills/computer-use/skill.json +5 -0
- package/skills/local-development/SKILL.md +73 -0
- package/src/relay-protocol.ts +29 -0
- package/worker/index.ts +670 -0
- package/worker/tsconfig.json +1 -0
- package/wrangler.jsonc +10 -0
package/dist/desktop.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { access } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
function windowsPaths(env) {
|
|
5
|
+
const values = [];
|
|
6
|
+
const add = (base, ...parts) => {
|
|
7
|
+
if (base)
|
|
8
|
+
values.push(join(base, ...parts));
|
|
9
|
+
};
|
|
10
|
+
add(env['ProgramFiles(x86)'], 'Microsoft', 'Edge', 'Application', 'msedge.exe');
|
|
11
|
+
add(env.ProgramFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe');
|
|
12
|
+
add(env.LOCALAPPDATA, 'Microsoft', 'Edge', 'Application', 'msedge.exe');
|
|
13
|
+
add(env['ProgramFiles(x86)'], 'Google', 'Chrome', 'Application', 'chrome.exe');
|
|
14
|
+
add(env.ProgramFiles, 'Google', 'Chrome', 'Application', 'chrome.exe');
|
|
15
|
+
add(env.LOCALAPPDATA, 'Google', 'Chrome', 'Application', 'chrome.exe');
|
|
16
|
+
return values;
|
|
17
|
+
}
|
|
18
|
+
export function desktopAppCandidates(url, platform = process.platform, env = process.env) {
|
|
19
|
+
const appArgs = [`--app=${url}`, '--new-window'];
|
|
20
|
+
if (env.LOCALMCP_DESKTOP_BROWSER) {
|
|
21
|
+
return [{
|
|
22
|
+
command: env.LOCALMCP_DESKTOP_BROWSER,
|
|
23
|
+
args: appArgs,
|
|
24
|
+
checkPath: false
|
|
25
|
+
}];
|
|
26
|
+
}
|
|
27
|
+
if (platform === 'win32') {
|
|
28
|
+
return [
|
|
29
|
+
...windowsPaths(env).map(command => ({ command, args: appArgs, checkPath: true })),
|
|
30
|
+
{ command: 'msedge.exe', args: appArgs, checkPath: false },
|
|
31
|
+
{ command: 'chrome.exe', args: appArgs, checkPath: false }
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
if (platform === 'darwin') {
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
command: 'open',
|
|
38
|
+
args: ['-na', 'Microsoft Edge', '--args', ...appArgs],
|
|
39
|
+
checkPath: false,
|
|
40
|
+
waitForExit: true
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
command: 'open',
|
|
44
|
+
args: ['-na', 'Google Chrome', '--args', ...appArgs],
|
|
45
|
+
checkPath: false,
|
|
46
|
+
waitForExit: true
|
|
47
|
+
}
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
return [
|
|
51
|
+
{ command: 'microsoft-edge', args: appArgs, checkPath: false },
|
|
52
|
+
{ command: 'google-chrome', args: appArgs, checkPath: false },
|
|
53
|
+
{ command: 'chromium', args: appArgs, checkPath: false },
|
|
54
|
+
{ command: 'chromium-browser', args: appArgs, checkPath: false }
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
export function defaultBrowserCandidate(url, platform = process.platform) {
|
|
58
|
+
if (platform === 'win32') {
|
|
59
|
+
return {
|
|
60
|
+
command: 'rundll32.exe',
|
|
61
|
+
args: ['url.dll,FileProtocolHandler', url],
|
|
62
|
+
waitForExit: false
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
if (platform === 'darwin') {
|
|
66
|
+
return {
|
|
67
|
+
command: 'open',
|
|
68
|
+
args: [url],
|
|
69
|
+
waitForExit: true
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
command: 'xdg-open',
|
|
74
|
+
args: [url],
|
|
75
|
+
waitForExit: true
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function exists(path) {
|
|
79
|
+
try {
|
|
80
|
+
await access(path);
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function launch(command, args, waitForExit = false) {
|
|
88
|
+
return await new Promise(resolveLaunch => {
|
|
89
|
+
const child = spawn(command, args, {
|
|
90
|
+
detached: !waitForExit,
|
|
91
|
+
stdio: 'ignore',
|
|
92
|
+
windowsHide: true
|
|
93
|
+
});
|
|
94
|
+
let settled = false;
|
|
95
|
+
const finish = (value) => {
|
|
96
|
+
if (settled)
|
|
97
|
+
return;
|
|
98
|
+
settled = true;
|
|
99
|
+
resolveLaunch(value);
|
|
100
|
+
};
|
|
101
|
+
child.once('error', () => finish(false));
|
|
102
|
+
child.once('spawn', () => {
|
|
103
|
+
if (!waitForExit) {
|
|
104
|
+
child.unref();
|
|
105
|
+
finish(true);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
if (waitForExit) {
|
|
109
|
+
child.once('exit', code => finish(code === 0));
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
export async function openDesktopControl(url, platform = process.platform, env = process.env) {
|
|
114
|
+
for (const candidate of desktopAppCandidates(url, platform, env)) {
|
|
115
|
+
if (candidate.checkPath && !(await exists(candidate.command)))
|
|
116
|
+
continue;
|
|
117
|
+
if (await launch(candidate.command, candidate.args, candidate.waitForExit === true)) {
|
|
118
|
+
return {
|
|
119
|
+
mode: 'app',
|
|
120
|
+
command: candidate.command
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const fallback = defaultBrowserCandidate(url, platform);
|
|
125
|
+
if (await launch(fallback.command, fallback.args, fallback.waitForExit)) {
|
|
126
|
+
return {
|
|
127
|
+
mode: 'browser',
|
|
128
|
+
command: fallback.command
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
throw new Error('Unable to open the Easy Local MCP desktop control window. '
|
|
132
|
+
+ 'Set LOCALMCP_DESKTOP_BROWSER to an Edge/Chrome executable or open the printed local UI URL manually.');
|
|
133
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import express from 'express';
|
|
5
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
7
|
+
import { config, configFilePath } from './config.js';
|
|
8
|
+
import { watchConfig } from './config-watch.js';
|
|
9
|
+
import { createServer } from './server.js';
|
|
10
|
+
import { McpLoader } from './mcp/loader.js';
|
|
11
|
+
import { loadSkills } from './skills/loader.js';
|
|
12
|
+
import { ProcessManager } from './process.js';
|
|
13
|
+
import { secureWriteFile } from './security.js';
|
|
14
|
+
async function ensureInitialized(force = false) {
|
|
15
|
+
const { access, cp, mkdir, realpath } = await import('node:fs/promises');
|
|
16
|
+
const { dirname, resolve } = await import('node:path');
|
|
17
|
+
const { homedir } = await import('node:os');
|
|
18
|
+
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const home = await realpath(resolve(homedir()));
|
|
20
|
+
const configDir = resolve(home, '.localmcp');
|
|
21
|
+
const target = resolve(configDir, 'localmcp.json');
|
|
22
|
+
const skillsTarget = resolve(configDir, 'skills');
|
|
23
|
+
await mkdir(configDir, { recursive: true, mode: 0o700 });
|
|
24
|
+
let created = false;
|
|
25
|
+
try {
|
|
26
|
+
await access(target);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code !== 'ENOENT')
|
|
30
|
+
throw error;
|
|
31
|
+
let workspaceRoot = await realpath(resolve(process.cwd()));
|
|
32
|
+
const sameHome = process.platform === 'win32'
|
|
33
|
+
? workspaceRoot.toLowerCase() === home.toLowerCase()
|
|
34
|
+
: workspaceRoot === home;
|
|
35
|
+
if (sameHome) {
|
|
36
|
+
workspaceRoot = resolve(configDir, 'workspace');
|
|
37
|
+
await mkdir(workspaceRoot, { recursive: true, mode: 0o700 });
|
|
38
|
+
}
|
|
39
|
+
const initialConfig = {
|
|
40
|
+
workspaces: {
|
|
41
|
+
project: workspaceRoot
|
|
42
|
+
},
|
|
43
|
+
defaultWorkspace: 'project',
|
|
44
|
+
features: {
|
|
45
|
+
files: {
|
|
46
|
+
read: true,
|
|
47
|
+
write: false,
|
|
48
|
+
delete: false
|
|
49
|
+
},
|
|
50
|
+
shell: false,
|
|
51
|
+
processes: false,
|
|
52
|
+
externalMcp: false
|
|
53
|
+
},
|
|
54
|
+
skills: {
|
|
55
|
+
dir: 'skills',
|
|
56
|
+
enabled: ['local-development']
|
|
57
|
+
},
|
|
58
|
+
mcpServers: {}
|
|
59
|
+
};
|
|
60
|
+
await secureWriteFile(target, JSON.stringify(initialConfig, null, 2) + '\n');
|
|
61
|
+
created = true;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await access(skillsTarget);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error.code !== 'ENOENT')
|
|
68
|
+
throw error;
|
|
69
|
+
await cp(resolve(packageRoot, 'skills'), skillsTarget, { recursive: true });
|
|
70
|
+
created = true;
|
|
71
|
+
}
|
|
72
|
+
if (created || force) {
|
|
73
|
+
console.log(created
|
|
74
|
+
? `Initialized ${configDir} with secure defaults.`
|
|
75
|
+
: `Already initialized: ${configDir}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function parseUnlockMinutes(args) {
|
|
79
|
+
const index = args.indexOf('--minutes');
|
|
80
|
+
if (index < 0)
|
|
81
|
+
return 30;
|
|
82
|
+
const raw = args[index + 1];
|
|
83
|
+
const minutes = Number(raw);
|
|
84
|
+
if (!raw || !Number.isInteger(minutes) || minutes < 1 || minutes > 480) {
|
|
85
|
+
throw new Error('--minutes must be an integer from 1 to 480');
|
|
86
|
+
}
|
|
87
|
+
return minutes;
|
|
88
|
+
}
|
|
89
|
+
async function main() {
|
|
90
|
+
const mode = process.argv[2] || 'start';
|
|
91
|
+
if (mode === 'init') {
|
|
92
|
+
await ensureInitialized(true);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (mode === 'ui' || mode === 'desktop' || mode === 'tray' || mode === 'desktop-host') {
|
|
96
|
+
await ensureInitialized();
|
|
97
|
+
const { startControlUi } = await import('./control-ui.js');
|
|
98
|
+
const rawPort = process.env.LOCALMCP_UI_PORT;
|
|
99
|
+
const port = rawPort === undefined ? 0 : Number(rawPort);
|
|
100
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
101
|
+
throw new Error('LOCALMCP_UI_PORT must be an integer from 0 to 65535');
|
|
102
|
+
}
|
|
103
|
+
const desktop = mode === 'desktop';
|
|
104
|
+
const tray = mode === 'tray';
|
|
105
|
+
const desktopHost = mode === 'desktop-host';
|
|
106
|
+
const ui = await startControlUi({
|
|
107
|
+
port,
|
|
108
|
+
openBrowser: !desktop && !tray && !desktopHost && !process.argv.slice(3).includes('--no-open')
|
|
109
|
+
});
|
|
110
|
+
if (tray) {
|
|
111
|
+
const { startNativeTray } = await import('./tray.js');
|
|
112
|
+
const session = await startNativeTray(ui.url);
|
|
113
|
+
console.log(`Easy Local MCP tray control: ${ui.url}`);
|
|
114
|
+
console.log(`Tray binary: ${session.command}`);
|
|
115
|
+
const close = () => {
|
|
116
|
+
session.child.kill();
|
|
117
|
+
void ui.close();
|
|
118
|
+
};
|
|
119
|
+
process.once('SIGINT', close);
|
|
120
|
+
process.once('SIGTERM', close);
|
|
121
|
+
const outcome = await Promise.race([
|
|
122
|
+
session.closed.then(code => ({ kind: 'tray', code })),
|
|
123
|
+
ui.closed.then(() => ({ kind: 'ui' }))
|
|
124
|
+
]);
|
|
125
|
+
if (outcome.kind === 'ui') {
|
|
126
|
+
session.child.kill();
|
|
127
|
+
await session.closed.catch(() => null);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
await ui.close();
|
|
131
|
+
if (outcome.code !== 0) {
|
|
132
|
+
throw new Error(`Easy Local MCP tray exited with code ${outcome.code ?? 'unknown'}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (desktopHost) {
|
|
138
|
+
console.log(`LOCALMCP_CONTROL_URL=${ui.url}`);
|
|
139
|
+
}
|
|
140
|
+
else if (desktop) {
|
|
141
|
+
const { openDesktopControl } = await import('./desktop.js');
|
|
142
|
+
const launched = await openDesktopControl(ui.url);
|
|
143
|
+
console.log(`Easy Local MCP desktop control: ${ui.url}`);
|
|
144
|
+
console.log(`Desktop window: ${launched.mode} via ${launched.command}`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
console.log(`Easy Local MCP control UI: ${ui.url}`);
|
|
148
|
+
}
|
|
149
|
+
const close = () => {
|
|
150
|
+
void ui.close();
|
|
151
|
+
};
|
|
152
|
+
process.once('SIGINT', close);
|
|
153
|
+
process.once('SIGTERM', close);
|
|
154
|
+
await ui.closed;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (['start', 'stop', 'reload', 'status', 'url', 'unlock', 'lock', 'rotate'].includes(mode)) {
|
|
158
|
+
const { control, status, printStatus, printUrl, request } = await import('./lifecycle.js');
|
|
159
|
+
if (mode === 'status') {
|
|
160
|
+
printStatus(await status());
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (mode === 'url') {
|
|
164
|
+
printUrl(await status());
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (mode === 'unlock') {
|
|
168
|
+
const current = await status();
|
|
169
|
+
if (current.status !== 'running') {
|
|
170
|
+
throw new Error('Easy Local MCP is stopped; start it before unlocking');
|
|
171
|
+
}
|
|
172
|
+
const minutes = parseUnlockMinutes(process.argv.slice(3));
|
|
173
|
+
printStatus(await request('unlock', { minutes }));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (mode === 'lock') {
|
|
177
|
+
const current = await status();
|
|
178
|
+
if (current.status !== 'running') {
|
|
179
|
+
throw new Error('Easy Local MCP is stopped');
|
|
180
|
+
}
|
|
181
|
+
printStatus(await request('lock'));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (mode === 'rotate') {
|
|
185
|
+
const current = await status();
|
|
186
|
+
if (current.status !== 'running') {
|
|
187
|
+
throw new Error('Easy Local MCP is stopped');
|
|
188
|
+
}
|
|
189
|
+
printStatus(await request('rotate'));
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
await control(mode, ensureInitialized);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (mode === 'agent') {
|
|
196
|
+
await ensureInitialized();
|
|
197
|
+
await import('./agent.js');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const cfg = await config();
|
|
201
|
+
if (!['stdio', 'http'].includes(mode)) {
|
|
202
|
+
throw new Error('Usage: easy-local-mcp [start|status|url|unlock|lock|rotate|stop|reload|ui|desktop|tray|init|agent|stdio|http]');
|
|
203
|
+
}
|
|
204
|
+
if (mode === 'http' && (!cfg.token || cfg.token.length < 32)) {
|
|
205
|
+
throw new Error('HTTP requires LOCALMCP_TOKEN with at least 32 characters');
|
|
206
|
+
}
|
|
207
|
+
const mcp = new McpLoader(cfg.mcpServers);
|
|
208
|
+
const skills = await loadSkills(cfg.skillsDir, cfg.enabledSkills);
|
|
209
|
+
const processes = new ProcessManager();
|
|
210
|
+
let runtime = {
|
|
211
|
+
config: cfg,
|
|
212
|
+
mcp,
|
|
213
|
+
skills
|
|
214
|
+
};
|
|
215
|
+
const retired = new Set();
|
|
216
|
+
const closeWatcher = watchConfig(configFilePath(), async (content) => {
|
|
217
|
+
const nextConfig = await config({
|
|
218
|
+
content,
|
|
219
|
+
path: configFilePath()
|
|
220
|
+
});
|
|
221
|
+
if (JSON.stringify(nextConfig) === JSON.stringify(runtime.config)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const nextSkills = await loadSkills(nextConfig.skillsDir, nextConfig.enabledSkills);
|
|
225
|
+
const changedMcp = JSON.stringify(nextConfig.mcpServers)
|
|
226
|
+
!== JSON.stringify(runtime.config.mcpServers);
|
|
227
|
+
const nextMcp = changedMcp
|
|
228
|
+
? new McpLoader(nextConfig.mcpServers)
|
|
229
|
+
: runtime.mcp;
|
|
230
|
+
const previous = runtime;
|
|
231
|
+
runtime = {
|
|
232
|
+
config: nextConfig,
|
|
233
|
+
mcp: nextMcp,
|
|
234
|
+
skills: nextSkills
|
|
235
|
+
};
|
|
236
|
+
if (changedMcp) {
|
|
237
|
+
const closing = previous.mcp
|
|
238
|
+
.close()
|
|
239
|
+
.finally(() => retired.delete(closing));
|
|
240
|
+
retired.add(closing);
|
|
241
|
+
}
|
|
242
|
+
console.error('Easy Local MCP configuration hot-reloaded.');
|
|
243
|
+
}, error => console.error('Easy Local MCP config hot-reload rejected; keeping current configuration:', error instanceof Error ? error.message : String(error)));
|
|
244
|
+
const shutdown = [];
|
|
245
|
+
if (mode === 'stdio') {
|
|
246
|
+
const server = await createServer(cfg, mcp, skills, processes, () => runtime);
|
|
247
|
+
await server.connect(new StdioServerTransport());
|
|
248
|
+
shutdown.push(() => server.close());
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const app = express();
|
|
252
|
+
app.disable('x-powered-by');
|
|
253
|
+
app.get('/healthz', (_req, res) => {
|
|
254
|
+
res.json({ ok: true });
|
|
255
|
+
});
|
|
256
|
+
app.use((req, res, next) => {
|
|
257
|
+
if (req.headers.origin) {
|
|
258
|
+
res.sendStatus(403);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const pathToken = /^\/mcp\/([A-Za-z0-9_-]+)$/.exec(req.path)?.[1];
|
|
262
|
+
const supplied = Buffer.from(pathToken
|
|
263
|
+
? `Bearer ${pathToken}`
|
|
264
|
+
: req.headers.authorization || '');
|
|
265
|
+
const expected = Buffer.from(`Bearer ${cfg.token}`);
|
|
266
|
+
if (supplied.length !== expected.length
|
|
267
|
+
|| !timingSafeEqual(supplied, expected)) {
|
|
268
|
+
res.sendStatus(404);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
272
|
+
next();
|
|
273
|
+
});
|
|
274
|
+
app.use(express.json({ limit: '2mb' }));
|
|
275
|
+
let gate = Promise.resolve();
|
|
276
|
+
app.post(['/mcp', '/mcp/:token'], async (req, res) => {
|
|
277
|
+
const previous = gate;
|
|
278
|
+
let release;
|
|
279
|
+
gate = new Promise(resolveGate => {
|
|
280
|
+
release = resolveGate;
|
|
281
|
+
});
|
|
282
|
+
await previous;
|
|
283
|
+
let server;
|
|
284
|
+
let transport;
|
|
285
|
+
try {
|
|
286
|
+
const active = runtime;
|
|
287
|
+
server = await createServer(active.config, active.mcp, active.skills, processes, () => runtime);
|
|
288
|
+
transport = new StreamableHTTPServerTransport({
|
|
289
|
+
sessionIdGenerator: undefined,
|
|
290
|
+
enableJsonResponse: true
|
|
291
|
+
});
|
|
292
|
+
await server.connect(transport);
|
|
293
|
+
await transport.handleRequest(req, res, req.body);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
if (!res.headersSent) {
|
|
297
|
+
res.status(500).json({
|
|
298
|
+
error: 'MCP request failed'
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
await transport?.close();
|
|
304
|
+
await server?.close();
|
|
305
|
+
release();
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
app.all(['/mcp', '/mcp/:token'], (_req, res) => {
|
|
309
|
+
res.setHeader('Allow', 'POST');
|
|
310
|
+
res.sendStatus(405);
|
|
311
|
+
});
|
|
312
|
+
const listener = app.listen(cfg.port, '127.0.0.1', () => {
|
|
313
|
+
if (process.env.LOCALMCP_INTERNAL !== '1') {
|
|
314
|
+
console.error(`Easy Local MCP listening on http://127.0.0.1:${cfg.port}/mcp`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
listener.on('error', error => {
|
|
318
|
+
console.error(error.message);
|
|
319
|
+
process.exit(1);
|
|
320
|
+
});
|
|
321
|
+
shutdown.push(() => new Promise(resolveClose => listener.close(() => resolveClose())));
|
|
322
|
+
}
|
|
323
|
+
const stop = async () => {
|
|
324
|
+
await closeWatcher();
|
|
325
|
+
for (const fn of shutdown) {
|
|
326
|
+
await fn();
|
|
327
|
+
}
|
|
328
|
+
await processes.close();
|
|
329
|
+
await runtime.mcp.close();
|
|
330
|
+
await Promise.allSettled([...retired]);
|
|
331
|
+
process.exit(0);
|
|
332
|
+
};
|
|
333
|
+
process.once('SIGINT', stop);
|
|
334
|
+
process.once('SIGTERM', stop);
|
|
335
|
+
}
|
|
336
|
+
main().catch(error => {
|
|
337
|
+
console.error(error.message);
|
|
338
|
+
process.exit(1);
|
|
339
|
+
});
|