Skip to content

Commit 28179a2

Browse files
committed
v0.2.18
1 parent 7f65110 commit 28179a2

File tree

7 files changed

+188
-17
lines changed

7 files changed

+188
-17
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@wonderwhy-er/desktop-commander",
3-
"version": "0.2.18-alpha.6",
3+
"version": "0.2.18",
44
"description": "MCP server for terminal operations and file editing",
55
"mcpName": "io.github.wonderwhy-er/desktop-commander",
66
"license": "MIT",
@@ -39,6 +39,7 @@
3939
"clean": "shx rm -rf dist",
4040
"test": "npm run build && node test/run-all-tests.js",
4141
"test:debug": "node --inspect test/run-all-tests.js",
42+
"validate:tools": "npm run build && node scripts/validate-tools-sync.js",
4243
"link:local": "npm run build && npm link",
4344
"unlink:local": "npm unlink",
4445
"inspector": "npx @modelcontextprotocol/inspector dist/index.js",

scripts/validate-tools-sync.js

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Validates that the tools listed in mcpb-bundle/manifest.json match
4+
* the tools actually provided by the running MCP server
5+
*
6+
* This uses JSON-RPC to query the server directly, avoiding fragile regex parsing.
7+
*/
8+
9+
import { readFile } from 'fs/promises';
10+
import { join, dirname } from 'path';
11+
import { fileURLToPath } from 'url';
12+
import { spawn } from 'child_process';
13+
14+
const __filename = fileURLToPath(import.meta.url);
15+
const __dirname = dirname(__filename);
16+
const rootDir = join(__dirname, '..');
17+
18+
// ANSI color codes for pretty output
19+
const colors = {
20+
reset: '\x1b[0m',
21+
red: '\x1b[31m',
22+
green: '\x1b[32m',
23+
yellow: '\x1b[33m',
24+
blue: '\x1b[34m',
25+
cyan: '\x1b[36m'
26+
};
27+
28+
async function extractToolsFromManifest() {
29+
const manifestPath = join(rootDir, 'mcpb-bundle', 'manifest.json');
30+
const content = await readFile(manifestPath, 'utf-8');
31+
const manifest = JSON.parse(content);
32+
33+
return manifest.tools.map(tool => tool.name).sort();
34+
}
35+
36+
async function extractToolsFromServer() {
37+
return new Promise((resolve, reject) => {
38+
// Start the MCP server
39+
const serverPath = join(rootDir, 'dist', 'index.js');
40+
const server = spawn('node', [serverPath], {
41+
stdio: ['pipe', 'pipe', 'pipe']
42+
});
43+
44+
let output = '';
45+
let errorOutput = '';
46+
const messages = [];
47+
48+
server.stdout.on('data', (data) => {
49+
output += data.toString();
50+
51+
// Try to parse each line as JSON-RPC message
52+
const lines = output.split('\n');
53+
output = lines.pop() || ''; // Keep incomplete line
54+
55+
for (const line of lines) {
56+
if (line.trim()) {
57+
try {
58+
messages.push(JSON.parse(line));
59+
} catch (e) {
60+
// Not JSON, might be debug output
61+
}
62+
}
63+
}
64+
});
65+
66+
server.stderr.on('data', (data) => {
67+
errorOutput += data.toString();
68+
});
69+
70+
// Step 1: Send initialize request
71+
const initRequest = {
72+
jsonrpc: '2.0',
73+
id: 1,
74+
method: 'initialize',
75+
params: {
76+
protocolVersion: '2024-11-05',
77+
capabilities: {},
78+
clientInfo: {
79+
name: 'validate-tools-sync',
80+
version: '1.0.0'
81+
}
82+
}
83+
};
84+
85+
server.stdin.write(JSON.stringify(initRequest) + '\n');
86+
87+
// Wait for initialize response, then send tools/list
88+
setTimeout(() => {
89+
// Step 2: Send tools/list request
90+
const toolsRequest = {
91+
jsonrpc: '2.0',
92+
id: 2,
93+
method: 'tools/list',
94+
params: {}
95+
};
96+
97+
server.stdin.write(JSON.stringify(toolsRequest) + '\n');
98+
99+
// Wait for tools/list response
100+
setTimeout(() => {
101+
server.kill();
102+
103+
// Find the tools/list response
104+
const toolsResponse = messages.find(msg => msg.id === 2 && msg.result);
105+
106+
if (!toolsResponse) {
107+
reject(new Error('No tools/list response received'));
108+
return;
109+
}
110+
111+
const tools = toolsResponse.result.tools.map(tool => tool.name).sort();
112+
resolve(tools);
113+
}, 1000);
114+
}, 500);
115+
116+
server.on('error', (error) => {
117+
reject(new Error(`Failed to start server: ${error.message}`));
118+
});
119+
});
120+
}
121+
122+
async function main() {
123+
console.log(`${colors.cyan}🔍 Validating tool synchronization...${colors.reset}\n`);
124+
125+
try {
126+
const manifestTools = await extractToolsFromManifest();
127+
const serverTools = await extractToolsFromServer();
128+
129+
console.log(`${colors.blue}📋 Manifest tools (${manifestTools.length}):${colors.reset}`);
130+
manifestTools.forEach(tool => console.log(` - ${tool}`));
131+
132+
console.log(`\n${colors.blue}⚙️ Server tools (${serverTools.length}):${colors.reset}`);
133+
serverTools.forEach(tool => console.log(` - ${tool}`));
134+
135+
// Find differences
136+
const missingInManifest = serverTools.filter(t => !manifestTools.includes(t));
137+
const missingInServer = manifestTools.filter(t => !serverTools.includes(t));
138+
139+
console.log('\n' + '='.repeat(60));
140+
141+
if (missingInManifest.length === 0 && missingInServer.length === 0) {
142+
console.log(`${colors.green}✅ SUCCESS: All tools are in sync!${colors.reset}`);
143+
console.log(`${colors.green} Both manifest.json and server.ts have ${manifestTools.length} tools.${colors.reset}`);
144+
process.exit(0);
145+
} else {
146+
console.log(`${colors.red}❌ MISMATCH DETECTED!${colors.reset}\n`);
147+
148+
if (missingInManifest.length > 0) {
149+
console.log(`${colors.yellow}⚠️ Tools in server.ts but NOT in manifest.json:${colors.reset}`);
150+
missingInManifest.forEach(tool => console.log(` ${colors.red}${colors.reset} ${tool}`));
151+
console.log();
152+
}
153+
154+
if (missingInServer.length > 0) {
155+
console.log(`${colors.yellow}⚠️ Tools in manifest.json but NOT in server.ts:${colors.reset}`);
156+
missingInServer.forEach(tool => console.log(` ${colors.red}${colors.reset} ${tool}`));
157+
console.log();
158+
}
159+
160+
console.log(`${colors.red}Please update the files to match!${colors.reset}`);
161+
process.exit(1);
162+
}
163+
164+
} catch (error) {
165+
console.error(`${colors.red}❌ Error:${colors.reset}`, error.message);
166+
process.exit(1);
167+
}
168+
}
169+
170+
main();

server.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
"url": "https://github.com/wonderwhy-er/DesktopCommanderMCP",
88
"source": "github"
99
},
10-
"version": "0.2.18-alpha.0",
10+
"version": "0.2.18",
1111
"packages": [
1212
{
1313
"registryType": "npm",
1414
"registryBaseUrl": "https://registry.npmjs.org",
1515
"identifier": "@wonderwhy-er/desktop-commander",
16-
"version": "0.2.18-alpha.0",
16+
"version": "0.2.18",
1717
"transport": {
1818
"type": "stdio"
1919
}

setup-claude-server.js

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -234,13 +234,16 @@ function getPackageSpec(versionArg = null) {
234234
return '@wonderwhy-er/desktop-commander@latest';
235235
}
236236

237+
function isNPX() {
238+
return process.env.npm_lifecycle_event === 'npx' ||
239+
process.env.npm_execpath?.includes('npx') ||
240+
process.env._?.includes('npx') ||
241+
import.meta.url.includes('node_modules');
242+
}
237243
// Function to determine execution context
238244
function getExecutionContext() {
239245
// Check if running from npx
240-
const isNpx = process.env.npm_lifecycle_event === 'npx' ||
241-
process.env.npm_execpath?.includes('npx') ||
242-
process.env._?.includes('npx') ||
243-
import.meta.url.includes('node_modules');
246+
const isNpx = isNPX();
244247

245248
// Check if installed globally
246249
const isGlobal = process.env.npm_config_global === 'true' ||
@@ -641,11 +644,8 @@ async function restartClaude() {
641644
// Main function to export for ESM compatibility
642645
export default async function setup() {
643646
// Parse command line arguments for version/tag
644-
// Usage: npx @wonderwhy-er/desktop-commander setup alpha
645-
// npx @wonderwhy-er/desktop-commander setup latest
646-
// npx @wonderwhy-er/desktop-commander setup 0.2.18
647-
const versionArg = process.argv[3]; // argv[0]=node, argv[1]=script, argv[2]=setup, argv[3]=version/tag
648-
647+
const versionArg = process.argv[3]; // argv[0]=node, argv[1]=script, argv[2]=version/tag
648+
649649
// Add tracking for setup function entry
650650
await trackEvent('npx_setup_function_started');
651651

@@ -744,7 +744,7 @@ export default async function setup() {
744744
const configPrepStep = addSetupStep('prepare_server_config');
745745

746746
// Determine if running through npx or locally
747-
const isNpx = import.meta.url.includes('node_modules');
747+
const isNpx = isNPX();
748748
await trackEvent('npx_setup_execution_mode', { isNpx });
749749

750750
// Fix Windows path handling for npx execution

src/utils/feature-flags.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class FeatureFlagManager {
1313
private flags: Record<string, any> = {};
1414
private lastFetch: number = 0;
1515
private cachePath: string;
16-
private cacheMaxAge: number = 30 * 60 * 1000; // 5 minutes - hardcoded refresh interval
16+
private cacheMaxAge: number = 30 * 60 * 1000;
1717
private flagUrl: string;
1818
private refreshInterval: NodeJS.Timeout | null = null;
1919

src/version.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const VERSION = '0.2.18-alpha.6';
1+
export const VERSION = '0.2.18';

0 commit comments

Comments
 (0)