@myelixlabs/sample-mcp 0.1.6
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/bin/sample-mcp.js +80 -0
- package/bin/synapse-mcp.js +4 -0
- package/lib/install.js +212 -0
- package/lib/mcp-config.js +220 -0
- package/lib/version.js +150 -0
- package/package.json +21 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { spawnSync } = require('child_process');
|
|
5
|
+
const { install, resolvePaths } = require('../lib/install');
|
|
6
|
+
const { offerMcpConfig } = require('../lib/mcp-config');
|
|
7
|
+
const { printVersionInfo } = require('../lib/version');
|
|
8
|
+
const pkg = require('../package.json');
|
|
9
|
+
|
|
10
|
+
const { binPath: BIN_PATH } = resolvePaths();
|
|
11
|
+
const PACKAGE_NAME = path.basename(process.argv[1] || '') === 'synapse-mcp' ? 'synapse-mcp' : pkg.name;
|
|
12
|
+
|
|
13
|
+
async function run(args) {
|
|
14
|
+
await install({ quiet: true, packageName: PACKAGE_NAME });
|
|
15
|
+
const result = spawnSync(BIN_PATH, args, { stdio: 'inherit' });
|
|
16
|
+
process.exit(result.status ?? 1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function usage() {
|
|
20
|
+
console.error('Usage: npx @myelixlabs/sample-mcp <command> [args]');
|
|
21
|
+
console.error('Commands:');
|
|
22
|
+
console.error(' install Download or update the Sample MCP launcher');
|
|
23
|
+
console.error(' setup Detect editors and offer to configure MCP servers');
|
|
24
|
+
console.error(' auth Start device sign-in');
|
|
25
|
+
console.error(' serve Run the MCP server');
|
|
26
|
+
console.error(' --version Print version');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const command = args[0];
|
|
32
|
+
|
|
33
|
+
if (command === '--version' || command === '-V' || command === 'version') {
|
|
34
|
+
printVersionInfo({ json: args.includes('--json') });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (command === 'install') {
|
|
39
|
+
const quiet = args.includes('--quiet');
|
|
40
|
+
await install({ quiet, packageName: PACKAGE_NAME });
|
|
41
|
+
if (!args.includes('--no-config')) {
|
|
42
|
+
await offerMcpConfig({
|
|
43
|
+
quiet,
|
|
44
|
+
yes:
|
|
45
|
+
args.includes('--yes') ||
|
|
46
|
+
args.includes('-y') ||
|
|
47
|
+
process.env.SAMPLE_MCP_CONFIG_YES === '1' ||
|
|
48
|
+
process.env.SYNAPSE_MCP_CONFIG_YES === '1',
|
|
49
|
+
command: BIN_PATH,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (command === 'setup' || command === 'configure') {
|
|
56
|
+
await install({ quiet: args.includes('--quiet'), packageName: PACKAGE_NAME });
|
|
57
|
+
await offerMcpConfig({
|
|
58
|
+
quiet: args.includes('--quiet'),
|
|
59
|
+
yes:
|
|
60
|
+
args.includes('--yes') ||
|
|
61
|
+
args.includes('-y') ||
|
|
62
|
+
process.env.SAMPLE_MCP_CONFIG_YES === '1' ||
|
|
63
|
+
process.env.SYNAPSE_MCP_CONFIG_YES === '1',
|
|
64
|
+
command: BIN_PATH,
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!command || command === '--help' || command === '-h') {
|
|
70
|
+
usage();
|
|
71
|
+
process.exit(command ? 0 : 1);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
await run(args);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
main().catch((err) => {
|
|
78
|
+
console.error(`sample-mcp: ${err.message}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
});
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
4
|
+
const childProcess = require('child_process');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_BASE_URL = 'https://downloads.synapse-mcp.dev/sample-mcp';
|
|
7
|
+
|
|
8
|
+
function env(name, fallbackName) {
|
|
9
|
+
return process.env[name] || (fallbackName ? process.env[fallbackName] : undefined);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function platformId() {
|
|
13
|
+
if (process.platform === 'linux' && process.arch === 'x64') return 'linux-x86_64';
|
|
14
|
+
if (process.platform === 'linux' && process.arch === 'arm64') return 'linux-aarch64';
|
|
15
|
+
if (process.platform === 'darwin' && process.arch === 'x64') return 'darwin-x86_64';
|
|
16
|
+
if (process.platform === 'darwin' && process.arch === 'arm64') return 'darwin-aarch64';
|
|
17
|
+
if (process.platform === 'win32' && process.arch === 'x64') return 'windows-x86_64.exe';
|
|
18
|
+
throw new Error(`Unsupported platform: ${process.platform} ${process.arch}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function resolvePaths(options = {}) {
|
|
22
|
+
const baseUrl =
|
|
23
|
+
options.baseUrl || env('SAMPLE_MCP_BASE_URL', 'SYNAPSE_MCP_BASE_URL') || DEFAULT_BASE_URL;
|
|
24
|
+
const installDir =
|
|
25
|
+
options.installDir ||
|
|
26
|
+
env('SAMPLE_MCP_INSTALL_DIR', 'SYNAPSE_MCP_INSTALL_DIR') ||
|
|
27
|
+
path.join(process.env.HOME || process.env.USERPROFILE, '.sample-mcp');
|
|
28
|
+
const binName = process.platform === 'win32' ? 'sample-mcp.exe' : 'sample-mcp';
|
|
29
|
+
const platform = env('SAMPLE_MCP_PLATFORM', 'SYNAPSE_MCP_PLATFORM') || platformId();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
baseUrl,
|
|
33
|
+
installDir,
|
|
34
|
+
binPath: path.join(installDir, binName),
|
|
35
|
+
platform,
|
|
36
|
+
fallbackUrl: `${baseUrl}/latest/sample-mcp-${platform}`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function sha256File(filePath) {
|
|
41
|
+
const hash = crypto.createHash('sha256');
|
|
42
|
+
hash.update(fs.readFileSync(filePath));
|
|
43
|
+
return hash.digest('hex');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function launcherVersion(binPath, spawnSync = childProcess.spawnSync) {
|
|
47
|
+
const result = spawnSync(binPath, ['--version'], { encoding: 'utf8' });
|
|
48
|
+
if (result.status !== 0) return null;
|
|
49
|
+
const match = `${result.stdout || ''}${result.stderr || ''}`.trim().match(/sample-mcp\s+(\S+)/);
|
|
50
|
+
return match ? match[1] : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function fetchManifest(baseUrl, fetchImpl = globalThis.fetch) {
|
|
54
|
+
const url = `${baseUrl}/manifest.json`;
|
|
55
|
+
try {
|
|
56
|
+
const init = {};
|
|
57
|
+
if (typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function') {
|
|
58
|
+
init.signal = AbortSignal.timeout(15000);
|
|
59
|
+
}
|
|
60
|
+
const res = await fetchImpl(url, init);
|
|
61
|
+
if (!res.ok) return null;
|
|
62
|
+
return await res.json();
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function downloadArtifact(url, binPath, quiet, spawnSync = childProcess.spawnSync) {
|
|
69
|
+
const tmp = `${binPath}.tmp`;
|
|
70
|
+
const result = spawnSync('curl', ['-fSL', url, '-o', tmp], { stdio: quiet ? 'pipe' : 'inherit' });
|
|
71
|
+
|
|
72
|
+
if (result.status !== 0) {
|
|
73
|
+
if (fs.existsSync(tmp)) fs.unlinkSync(tmp);
|
|
74
|
+
throw new Error(`Failed to download Sample MCP from ${url}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (process.platform !== 'win32') fs.chmodSync(tmp, 0o755);
|
|
78
|
+
fs.renameSync(tmp, binPath);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function versionLabel(releaseVersion, launcherVersionValue) {
|
|
82
|
+
if (releaseVersion) return releaseVersion;
|
|
83
|
+
if (launcherVersionValue) return launcherVersionValue;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function formatStatus(action, { releaseVersion, launcherVersion: launcherVersionValue, binPath }) {
|
|
88
|
+
const version = versionLabel(releaseVersion, launcherVersionValue);
|
|
89
|
+
const suffix = version ? ` (${version})` : '';
|
|
90
|
+
|
|
91
|
+
switch (action) {
|
|
92
|
+
case 'already_installed':
|
|
93
|
+
return `Sample MCP is already installed${suffix} at ${binPath}`;
|
|
94
|
+
case 'updated':
|
|
95
|
+
return `Sample MCP updated${suffix} at ${binPath}`;
|
|
96
|
+
case 'installed':
|
|
97
|
+
return `Sample MCP installed${suffix} to ${binPath}`;
|
|
98
|
+
default:
|
|
99
|
+
throw new Error(`Unknown install status: ${action}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function formatAuthNextStep(packageName = 'sample-mcp') {
|
|
104
|
+
const scopedPackage = packageName.startsWith('@') ? packageName : `@myelixlabs/${packageName}`;
|
|
105
|
+
return `Next: run \`npx ${scopedPackage} auth\` to sign in and activate your license.`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Ensure the Sample MCP launcher is present and up to date.
|
|
110
|
+
*/
|
|
111
|
+
async function install(options = {}) {
|
|
112
|
+
const quiet = Boolean(options.quiet);
|
|
113
|
+
const spawnSync = options.spawnSync || childProcess.spawnSync;
|
|
114
|
+
const { baseUrl, installDir, binPath, platform, fallbackUrl } = resolvePaths(options);
|
|
115
|
+
const fetchImpl = options.fetch || globalThis.fetch;
|
|
116
|
+
|
|
117
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
118
|
+
|
|
119
|
+
const exists = fs.existsSync(binPath);
|
|
120
|
+
const installedLauncherVersion = exists ? launcherVersion(binPath, spawnSync) : null;
|
|
121
|
+
const manifest = await fetchManifest(baseUrl, fetchImpl);
|
|
122
|
+
const launcherArtifact = manifest?.artifacts?.[platform]?.launcher;
|
|
123
|
+
const releaseVersion = manifest?.latest ?? null;
|
|
124
|
+
const downloadUrl = launcherArtifact?.url || fallbackUrl;
|
|
125
|
+
const expectedSha = launcherArtifact?.sha256;
|
|
126
|
+
const updateCheckFailed = !manifest;
|
|
127
|
+
|
|
128
|
+
if (exists && expectedSha) {
|
|
129
|
+
const localSha = sha256File(binPath);
|
|
130
|
+
if (localSha === expectedSha) {
|
|
131
|
+
const message = formatStatus('already_installed', {
|
|
132
|
+
releaseVersion,
|
|
133
|
+
launcherVersion: installedLauncherVersion,
|
|
134
|
+
binPath,
|
|
135
|
+
});
|
|
136
|
+
if (!quiet) {
|
|
137
|
+
console.error(message);
|
|
138
|
+
console.error(formatAuthNextStep(options.packageName));
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
status: 'already_installed',
|
|
142
|
+
binPath,
|
|
143
|
+
releaseVersion,
|
|
144
|
+
launcherVersion: installedLauncherVersion,
|
|
145
|
+
updateCheckFailed,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
} else if (exists && updateCheckFailed) {
|
|
149
|
+
const message = formatStatus('already_installed', {
|
|
150
|
+
releaseVersion: null,
|
|
151
|
+
launcherVersion: installedLauncherVersion,
|
|
152
|
+
binPath,
|
|
153
|
+
});
|
|
154
|
+
if (!quiet) {
|
|
155
|
+
console.error(message);
|
|
156
|
+
console.error('Could not check for updates (release manifest unavailable).');
|
|
157
|
+
console.error(formatAuthNextStep(options.packageName));
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
status: 'already_installed',
|
|
161
|
+
binPath,
|
|
162
|
+
releaseVersion: null,
|
|
163
|
+
launcherVersion: installedLauncherVersion,
|
|
164
|
+
updateCheckFailed: true,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!quiet) {
|
|
169
|
+
console.error(exists ? 'Downloading Sample MCP update...' : 'Downloading Sample MCP...');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
downloadArtifact(downloadUrl, binPath, quiet, spawnSync);
|
|
173
|
+
|
|
174
|
+
if (expectedSha) {
|
|
175
|
+
const downloadedSha = sha256File(binPath);
|
|
176
|
+
if (downloadedSha !== expectedSha) {
|
|
177
|
+
fs.unlinkSync(binPath);
|
|
178
|
+
throw new Error('Downloaded Sample MCP launcher failed integrity check');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const status = exists ? 'updated' : 'installed';
|
|
183
|
+
const newLauncherVersion = launcherVersion(binPath, spawnSync);
|
|
184
|
+
const message = formatStatus(status, {
|
|
185
|
+
releaseVersion,
|
|
186
|
+
launcherVersion: newLauncherVersion,
|
|
187
|
+
binPath,
|
|
188
|
+
});
|
|
189
|
+
if (!quiet) {
|
|
190
|
+
console.error(message);
|
|
191
|
+
console.error(formatAuthNextStep(options.packageName));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
status,
|
|
196
|
+
binPath,
|
|
197
|
+
releaseVersion,
|
|
198
|
+
launcherVersion: newLauncherVersion,
|
|
199
|
+
updateCheckFailed,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
module.exports = {
|
|
204
|
+
DEFAULT_BASE_URL,
|
|
205
|
+
formatAuthNextStep,
|
|
206
|
+
formatStatus,
|
|
207
|
+
install,
|
|
208
|
+
launcherVersion,
|
|
209
|
+
platformId,
|
|
210
|
+
resolvePaths,
|
|
211
|
+
sha256File,
|
|
212
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const readline = require('readline');
|
|
5
|
+
|
|
6
|
+
const SERVER_NAME = 'sample-mcp';
|
|
7
|
+
|
|
8
|
+
function homeDir() {
|
|
9
|
+
return process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function appDataDir() {
|
|
13
|
+
if (process.platform === 'win32') return process.env.APPDATA || path.join(homeDir(), 'AppData', 'Roaming');
|
|
14
|
+
if (process.platform === 'darwin') return path.join(homeDir(), 'Library', 'Application Support');
|
|
15
|
+
return process.env.XDG_CONFIG_HOME || path.join(homeDir(), '.config');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function sampleServerConfig(command = 'sample-mcp') {
|
|
19
|
+
return { command, args: ['serve'] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function jsonConfig(id, label, configPath, section = 'mcpServers') {
|
|
23
|
+
return { id, label, type: 'json', configPath, section };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function codexConfig(configPath) {
|
|
27
|
+
return { id: 'codex', label: 'Codex', type: 'toml', configPath };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function candidateConfigs(options = {}) {
|
|
31
|
+
const cwd = options.cwd || process.cwd();
|
|
32
|
+
const home = options.home || homeDir();
|
|
33
|
+
const appData = options.appData || appDataDir();
|
|
34
|
+
|
|
35
|
+
return [
|
|
36
|
+
jsonConfig('cursor', 'Cursor', path.join(home, '.cursor', 'mcp.json')),
|
|
37
|
+
jsonConfig('cursor', 'Cursor', path.join(appData, 'Cursor', 'User', 'mcp.json')),
|
|
38
|
+
jsonConfig('antigravity', 'Antigravity', path.join(home, '.antigravity', 'mcp.json')),
|
|
39
|
+
jsonConfig('antigravity', 'Antigravity', path.join(appData, 'Antigravity', 'User', 'mcp.json')),
|
|
40
|
+
jsonConfig('claude-code', 'Claude Code', path.join(home, '.claude.json')),
|
|
41
|
+
jsonConfig('claude-code', 'Claude Code', path.join(home, '.claude', 'mcp.json')),
|
|
42
|
+
jsonConfig('claude-code', 'Claude Code', path.join(cwd, '.mcp.json')),
|
|
43
|
+
codexConfig(path.join(home, '.codex', 'config.toml')),
|
|
44
|
+
codexConfig(path.join(appData, 'Codex', 'config.toml')),
|
|
45
|
+
jsonConfig('windsurf', 'Windsurf', path.join(home, '.codeium', 'windsurf', 'mcp_config.json')),
|
|
46
|
+
jsonConfig('vscode', 'VS Code', path.join(appData, 'Code', 'User', 'mcp.json')),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function readJson(file) {
|
|
51
|
+
if (!fs.existsSync(file)) return null;
|
|
52
|
+
const text = fs.readFileSync(file, 'utf8').trim();
|
|
53
|
+
if (!text) return {};
|
|
54
|
+
return JSON.parse(text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function hasSampleJson(data, section = 'mcpServers') {
|
|
58
|
+
if (!data || typeof data !== 'object') return false;
|
|
59
|
+
const servers = data[section];
|
|
60
|
+
if (servers && typeof servers === 'object') {
|
|
61
|
+
if (servers[SERVER_NAME]) return true;
|
|
62
|
+
return Object.entries(servers).some(([name, server]) => {
|
|
63
|
+
if (String(name).toLowerCase() === SERVER_NAME) return true;
|
|
64
|
+
if (String(name).toLowerCase() === 'synapse') return true;
|
|
65
|
+
return (
|
|
66
|
+
server &&
|
|
67
|
+
typeof server === 'object' &&
|
|
68
|
+
typeof server.command === 'string' &&
|
|
69
|
+
/(sample-mcp|synapse-mcp)/.test(server.command)
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return Object.values(data).some((value) => {
|
|
75
|
+
if (!value || typeof value !== 'object') return false;
|
|
76
|
+
return hasSampleJson(value, section);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function hasSampleToml(text) {
|
|
81
|
+
return (
|
|
82
|
+
/\[\s*mcp_servers\.sample-mcp\s*\]/.test(text) ||
|
|
83
|
+
/\[\s*mcp_servers\.synapse\s*\]/.test(text) ||
|
|
84
|
+
/(sample-mcp|synapse-mcp)/.test(text)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function discoverHarnesses(options = {}) {
|
|
89
|
+
const seen = new Set();
|
|
90
|
+
const discovered = [];
|
|
91
|
+
|
|
92
|
+
for (const candidate of candidateConfigs(options)) {
|
|
93
|
+
const key = `${candidate.id}:${candidate.configPath}`;
|
|
94
|
+
if (seen.has(key)) continue;
|
|
95
|
+
seen.add(key);
|
|
96
|
+
if (!fs.existsSync(candidate.configPath)) continue;
|
|
97
|
+
|
|
98
|
+
let hasSample = false;
|
|
99
|
+
try {
|
|
100
|
+
if (candidate.type === 'json') {
|
|
101
|
+
hasSample = hasSampleJson(readJson(candidate.configPath), candidate.section);
|
|
102
|
+
} else if (candidate.type === 'toml') {
|
|
103
|
+
hasSample = hasSampleToml(fs.readFileSync(candidate.configPath, 'utf8'));
|
|
104
|
+
}
|
|
105
|
+
} catch (err) {
|
|
106
|
+
discovered.push({ ...candidate, installed: true, hasSample: false, parseError: err.message });
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
discovered.push({ ...candidate, installed: true, hasSample });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return discovered;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function writeJson(file, data) {
|
|
117
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
118
|
+
fs.writeFileSync(file, `${JSON.stringify(data, null, 2)}\n`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function configureJsonHarness(harness, serverConfig) {
|
|
122
|
+
const current = readJson(harness.configPath) || {};
|
|
123
|
+
if (hasSampleJson(current, harness.section)) return false;
|
|
124
|
+
if (!current[harness.section] || typeof current[harness.section] !== 'object') {
|
|
125
|
+
current[harness.section] = {};
|
|
126
|
+
}
|
|
127
|
+
current[harness.section][SERVER_NAME] = serverConfig;
|
|
128
|
+
writeJson(harness.configPath, current);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function configureTomlHarness(harness, serverConfig) {
|
|
133
|
+
let current = fs.existsSync(harness.configPath) ? fs.readFileSync(harness.configPath, 'utf8') : '';
|
|
134
|
+
if (hasSampleToml(current)) return false;
|
|
135
|
+
if (current.length && !current.endsWith('\n')) current += '\n';
|
|
136
|
+
const args = JSON.stringify(serverConfig.args || ['serve']);
|
|
137
|
+
const block = [
|
|
138
|
+
'',
|
|
139
|
+
'[mcp_servers.sample-mcp]',
|
|
140
|
+
`command = ${JSON.stringify(serverConfig.command)}`,
|
|
141
|
+
`args = ${args}`,
|
|
142
|
+
'',
|
|
143
|
+
].join('\n');
|
|
144
|
+
fs.mkdirSync(path.dirname(harness.configPath), { recursive: true });
|
|
145
|
+
fs.writeFileSync(harness.configPath, current + block);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function configureHarness(harness, options = {}) {
|
|
150
|
+
const serverConfig = sampleServerConfig(options.command || 'sample-mcp');
|
|
151
|
+
if (harness.type === 'json') return configureJsonHarness(harness, serverConfig);
|
|
152
|
+
if (harness.type === 'toml') return configureTomlHarness(harness, serverConfig);
|
|
153
|
+
throw new Error(`Unsupported harness config type: ${harness.type}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function ask(question, rl) {
|
|
157
|
+
return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function offerMcpConfig(options = {}) {
|
|
161
|
+
const quiet = Boolean(options.quiet);
|
|
162
|
+
if (quiet) return [];
|
|
163
|
+
|
|
164
|
+
const harnesses = discoverHarnesses(options);
|
|
165
|
+
if (harnesses.length === 0) {
|
|
166
|
+
console.error(
|
|
167
|
+
'No supported MCP editor configs found. Supported: Cursor, Antigravity, Claude Code, Codex, Windsurf, VS Code.'
|
|
168
|
+
);
|
|
169
|
+
return [];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const pending = harnesses.filter((h) => !h.hasSample && !h.parseError);
|
|
173
|
+
const alreadyConfigured = harnesses.filter((h) => h.hasSample);
|
|
174
|
+
for (const harness of alreadyConfigured) {
|
|
175
|
+
console.error(`Sample MCP is already configured for ${harness.label}: ${harness.configPath}`);
|
|
176
|
+
}
|
|
177
|
+
for (const harness of harnesses.filter((h) => h.parseError)) {
|
|
178
|
+
console.error(`Skipping ${harness.label}; could not parse ${harness.configPath}: ${harness.parseError}`);
|
|
179
|
+
}
|
|
180
|
+
if (pending.length === 0) return [];
|
|
181
|
+
|
|
182
|
+
const yes = Boolean(options.yes);
|
|
183
|
+
const rl = yes ? null : readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
184
|
+
const configured = [];
|
|
185
|
+
try {
|
|
186
|
+
for (const harness of pending) {
|
|
187
|
+
let shouldConfigure = yes;
|
|
188
|
+
if (!yes) {
|
|
189
|
+
const answer = await ask(
|
|
190
|
+
`Add Sample MCP to ${harness.label} (${harness.configPath})? [Y/n] `,
|
|
191
|
+
rl
|
|
192
|
+
);
|
|
193
|
+
shouldConfigure = answer === '' || /^y(es)?$/i.test(answer);
|
|
194
|
+
}
|
|
195
|
+
if (!shouldConfigure) continue;
|
|
196
|
+
if (configureHarness(harness, { command: options.command || 'sample-mcp' })) {
|
|
197
|
+
configured.push(harness);
|
|
198
|
+
console.error(`Configured Sample MCP for ${harness.label}: ${harness.configPath}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} finally {
|
|
202
|
+
if (rl) rl.close();
|
|
203
|
+
}
|
|
204
|
+
return configured;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
module.exports = {
|
|
208
|
+
SERVER_NAME,
|
|
209
|
+
candidateConfigs,
|
|
210
|
+
configureHarness,
|
|
211
|
+
discoverHarnesses,
|
|
212
|
+
hasSampleJson,
|
|
213
|
+
hasSampleToml,
|
|
214
|
+
offerMcpConfig,
|
|
215
|
+
sampleServerConfig,
|
|
216
|
+
// Backward-compatible aliases
|
|
217
|
+
hasSynapseJson: hasSampleJson,
|
|
218
|
+
hasSynapseToml: hasSampleToml,
|
|
219
|
+
synapseServerConfig: sampleServerConfig,
|
|
220
|
+
};
|
package/lib/version.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const childProcess = require('child_process');
|
|
5
|
+
const { launcherVersion, resolvePaths } = require('./install');
|
|
6
|
+
|
|
7
|
+
const RUNTIME_NAME = 'sample_mcp';
|
|
8
|
+
|
|
9
|
+
function runtimeCacheRoots() {
|
|
10
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
11
|
+
const roots = [path.join(home, '.local', 'share', 'sample-mcp', 'base')];
|
|
12
|
+
|
|
13
|
+
if (process.platform === 'darwin') {
|
|
14
|
+
roots.push(path.join(home, 'Library', 'Application Support', 'sample-mcp', 'base'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (process.env.LOCALAPPDATA) {
|
|
18
|
+
roots.push(path.join(process.env.LOCALAPPDATA, 'sample-mcp', 'base'));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Legacy Synapse-branded cache locations
|
|
22
|
+
roots.push(path.join(home, '.local', 'share', 'synapse-mcp', 'base'));
|
|
23
|
+
if (process.platform === 'darwin') {
|
|
24
|
+
roots.push(path.join(home, 'Library', 'Application Support', 'synapse-mcp', 'base'));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return roots;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function detectDownloadedRuntime() {
|
|
31
|
+
const encNames =
|
|
32
|
+
process.platform === 'win32'
|
|
33
|
+
? ['sample_mcp.exe.enc', 'synapse_mcp.exe.enc']
|
|
34
|
+
: ['sample_mcp.enc', 'synapse_mcp.enc'];
|
|
35
|
+
|
|
36
|
+
for (const baseDir of runtimeCacheRoots()) {
|
|
37
|
+
if (!fs.existsSync(baseDir)) continue;
|
|
38
|
+
|
|
39
|
+
const versions = fs.readdirSync(baseDir).filter((entry) => {
|
|
40
|
+
return encNames.some((name) => fs.existsSync(path.join(baseDir, entry, name)));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (versions.length === 0) continue;
|
|
44
|
+
|
|
45
|
+
versions.sort((a, b) => b.localeCompare(a));
|
|
46
|
+
return versions[0];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function launcherVersionJson(binPath, spawnSync = childProcess.spawnSync) {
|
|
53
|
+
const result = spawnSync(binPath, ['version', '--json'], { encoding: 'utf8' });
|
|
54
|
+
if (result.status !== 0) return null;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(`${result.stdout || ''}`.trim());
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function collectVersionInfo(options = {}) {
|
|
64
|
+
const pkg = options.pkg || require('../package.json');
|
|
65
|
+
const spawnSync = options.spawnSync || childProcess.spawnSync;
|
|
66
|
+
const { binPath } = resolvePaths(options);
|
|
67
|
+
|
|
68
|
+
const info = {
|
|
69
|
+
shim: {
|
|
70
|
+
name: '@myelixlabs/sample-mcp',
|
|
71
|
+
version: pkg.version,
|
|
72
|
+
},
|
|
73
|
+
launcher: {
|
|
74
|
+
installed: false,
|
|
75
|
+
version: null,
|
|
76
|
+
path: binPath,
|
|
77
|
+
},
|
|
78
|
+
runtime: {
|
|
79
|
+
name: RUNTIME_NAME,
|
|
80
|
+
version: null,
|
|
81
|
+
downloaded: false,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (!fs.existsSync(binPath)) {
|
|
86
|
+
return info;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
info.launcher.installed = true;
|
|
90
|
+
info.launcher.version = launcherVersion(binPath, spawnSync);
|
|
91
|
+
|
|
92
|
+
const launcherJson = launcherVersionJson(binPath, spawnSync);
|
|
93
|
+
if (launcherJson) {
|
|
94
|
+
if (launcherJson.launcher_version) {
|
|
95
|
+
info.launcher.version = launcherJson.launcher_version;
|
|
96
|
+
}
|
|
97
|
+
if (launcherJson.runtime_downloaded && launcherJson.resolved_version) {
|
|
98
|
+
info.runtime.downloaded = true;
|
|
99
|
+
info.runtime.version = launcherJson.resolved_version;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!info.runtime.downloaded) {
|
|
104
|
+
const cachedVersion = detectDownloadedRuntime();
|
|
105
|
+
if (cachedVersion) {
|
|
106
|
+
info.runtime.downloaded = true;
|
|
107
|
+
info.runtime.version = cachedVersion;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return info;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatVersionInfo(info) {
|
|
115
|
+
const lines = [`sample-mcp npm shim: ${info.shim.version}`];
|
|
116
|
+
|
|
117
|
+
if (info.launcher.installed) {
|
|
118
|
+
lines.push(`sample-mcp launcher: ${info.launcher.version || 'unknown'} (${info.launcher.path})`);
|
|
119
|
+
} else {
|
|
120
|
+
lines.push('sample-mcp launcher: not installed');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (info.runtime.downloaded) {
|
|
124
|
+
lines.push(`${info.runtime.name} runtime: ${info.runtime.version} (downloaded)`);
|
|
125
|
+
} else {
|
|
126
|
+
lines.push(`${info.runtime.name} runtime: not downloaded`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return lines.join('\n');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function printVersionInfo(options = {}) {
|
|
133
|
+
const info = collectVersionInfo(options);
|
|
134
|
+
if (options.json) {
|
|
135
|
+
console.log(JSON.stringify(info, null, 2));
|
|
136
|
+
return info;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(formatVersionInfo(info));
|
|
140
|
+
return info;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
RUNTIME_NAME,
|
|
145
|
+
collectVersionInfo,
|
|
146
|
+
detectDownloadedRuntime,
|
|
147
|
+
formatVersionInfo,
|
|
148
|
+
launcherVersionJson,
|
|
149
|
+
printVersionInfo,
|
|
150
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@myelixlabs/sample-mcp",
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"description": "POC installer/launcher for the Sample MCP precompiled binary.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"sample-mcp": "bin/sample-mcp.js",
|
|
7
|
+
"synapse-mcp": "bin/synapse-mcp.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"postinstall": "node bin/sample-mcp.js install --quiet",
|
|
11
|
+
"test": "node --test test/*.test.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin/",
|
|
15
|
+
"lib/"
|
|
16
|
+
],
|
|
17
|
+
"license": "UNLICENSED",
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
}
|
|
21
|
+
}
|