@khanglvm/tool-hub-mcp 1.0.0
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/README.md +87 -0
- package/cli.js +130 -0
- package/package.json +45 -0
- package/postinstall.js +197 -0
package/README.md
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# @khanglvm/tool-hub-mcp
|
|
2
|
+
|
|
3
|
+
**Serverless MCP Aggregator** - Reduce AI context token consumption by 60-97%
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Zero-install usage (recommended)
|
|
9
|
+
npx @khanglvm/tool-hub-mcp setup
|
|
10
|
+
npx @khanglvm/tool-hub-mcp serve
|
|
11
|
+
npx @khanglvm/tool-hub-mcp benchmark
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Also works with other JS package runners:
|
|
15
|
+
```bash
|
|
16
|
+
bunx @khanglvm/tool-hub-mcp setup
|
|
17
|
+
pnpm dlx @khanglvm/tool-hub-mcp setup
|
|
18
|
+
yarn dlx @khanglvm/tool-hub-mcp setup
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## What It Does
|
|
22
|
+
|
|
23
|
+
When using multiple MCP servers with AI clients (Claude Code, OpenCode, etc.), each server exposes all its tools to the AI. With 5+ servers, you can easily consume **25,000+ tokens** just for tool definitions.
|
|
24
|
+
|
|
25
|
+
`tool-hub-mcp` acts as a single MCP endpoint that exposes only **5 meta-tools**:
|
|
26
|
+
|
|
27
|
+
| Tool | Description |
|
|
28
|
+
|------|-------------|
|
|
29
|
+
| `hub_list` | List all registered MCP servers |
|
|
30
|
+
| `hub_discover` | Get tools from a specific server |
|
|
31
|
+
| `hub_search` | Search for tools across servers |
|
|
32
|
+
| `hub_execute` | Execute a tool from a server |
|
|
33
|
+
| `hub_help` | Get detailed help for a tool |
|
|
34
|
+
|
|
35
|
+
**Result:** ~461 tokens instead of 1,200-25,000+ = **61-97% reduction**
|
|
36
|
+
|
|
37
|
+
## Commands
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Import configs from AI CLI tools (Claude Code, OpenCode, etc.)
|
|
41
|
+
npx @khanglvm/tool-hub-mcp setup
|
|
42
|
+
|
|
43
|
+
# Run as MCP server (for AI client integration)
|
|
44
|
+
npx @khanglvm/tool-hub-mcp serve
|
|
45
|
+
|
|
46
|
+
# Add MCP servers manually
|
|
47
|
+
npx @khanglvm/tool-hub-mcp add jira --command npx --arg -y --arg @lvmk/jira-mcp
|
|
48
|
+
|
|
49
|
+
# Run token efficiency benchmark
|
|
50
|
+
npx @khanglvm/tool-hub-mcp benchmark
|
|
51
|
+
|
|
52
|
+
# Run speed/latency benchmark
|
|
53
|
+
npx @khanglvm/tool-hub-mcp benchmark speed
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## MCP Client Configuration
|
|
57
|
+
|
|
58
|
+
Add to your AI client config:
|
|
59
|
+
|
|
60
|
+
**Claude Code:**
|
|
61
|
+
```bash
|
|
62
|
+
claude mcp add tool-hub -- npx @khanglvm/tool-hub-mcp serve
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Manual JSON config:**
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"tool-hub": {
|
|
70
|
+
"command": "npx",
|
|
71
|
+
"args": ["@khanglvm/tool-hub-mcp", "serve"]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Platforms
|
|
78
|
+
|
|
79
|
+
This package automatically installs the correct binary for your platform:
|
|
80
|
+
|
|
81
|
+
- macOS (Apple Silicon & Intel)
|
|
82
|
+
- Linux (x64 & ARM64)
|
|
83
|
+
- Windows (x64)
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT © khanglvm
|
package/cli.js
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CLI wrapper for tool-hub-mcp Go binary.
|
|
5
|
+
* Locates the platform-specific binary from optionalDependencies and spawns it.
|
|
6
|
+
*
|
|
7
|
+
* Architecture: Thin wrapper that passes through all args and stdio to the Go binary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the platform-specific package name based on current OS and architecture.
|
|
17
|
+
* @returns {string} Package name like '@khanglvm/tool-hub-mcp-darwin-arm64'
|
|
18
|
+
*/
|
|
19
|
+
function getPlatformPackageName() {
|
|
20
|
+
const platform = os.platform();
|
|
21
|
+
const arch = os.arch();
|
|
22
|
+
|
|
23
|
+
// Map Node.js platform/arch to our package naming convention
|
|
24
|
+
const platformMap = {
|
|
25
|
+
'darwin': 'darwin',
|
|
26
|
+
'linux': 'linux',
|
|
27
|
+
'win32': 'win32'
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const archMap = {
|
|
31
|
+
'arm64': 'arm64',
|
|
32
|
+
'x64': 'x64',
|
|
33
|
+
'x86_64': 'x64' // Fallback
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const mappedPlatform = platformMap[platform];
|
|
37
|
+
const mappedArch = archMap[arch];
|
|
38
|
+
|
|
39
|
+
if (!mappedPlatform || !mappedArch) {
|
|
40
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return `@khanglvm/tool-hub-mcp-${mappedPlatform}-${mappedArch}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find the binary path from the installed platform package.
|
|
48
|
+
* Searches in node_modules relative to this script.
|
|
49
|
+
* @returns {string|null} Path to binary or null if not found
|
|
50
|
+
*/
|
|
51
|
+
function findBinaryFromPackage() {
|
|
52
|
+
const packageName = getPlatformPackageName();
|
|
53
|
+
const binaryName = os.platform() === 'win32' ? 'tool-hub-mcp.exe' : 'tool-hub-mcp';
|
|
54
|
+
|
|
55
|
+
// Get platform directory name (e.g., 'darwin-arm64')
|
|
56
|
+
const platformDir = packageName.replace('@khanglvm/tool-hub-mcp-', '');
|
|
57
|
+
|
|
58
|
+
// Search locations (support various node_modules layouts + local dev)
|
|
59
|
+
const searchPaths = [
|
|
60
|
+
// Local development (platforms subdirectory)
|
|
61
|
+
path.join(__dirname, 'platforms', platformDir, 'bin', binaryName),
|
|
62
|
+
// Standard node_modules
|
|
63
|
+
path.join(__dirname, 'node_modules', packageName, 'bin', binaryName),
|
|
64
|
+
// Hoisted (pnpm, yarn workspaces)
|
|
65
|
+
path.join(__dirname, '..', packageName, 'bin', binaryName),
|
|
66
|
+
// Global install
|
|
67
|
+
path.join(__dirname, '..', '..', packageName, 'bin', binaryName),
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
for (const searchPath of searchPaths) {
|
|
71
|
+
if (fs.existsSync(searchPath)) {
|
|
72
|
+
return searchPath;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Find binary downloaded by postinstall fallback.
|
|
81
|
+
* @returns {string|null} Path to binary or null if not found
|
|
82
|
+
*/
|
|
83
|
+
function findDownloadedBinary() {
|
|
84
|
+
const binaryName = os.platform() === 'win32' ? 'tool-hub-mcp.exe' : 'tool-hub-mcp';
|
|
85
|
+
const downloadPath = path.join(__dirname, 'bin', binaryName);
|
|
86
|
+
|
|
87
|
+
if (fs.existsSync(downloadPath)) {
|
|
88
|
+
return downloadPath;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Main entry point - find binary and spawn with all arguments.
|
|
96
|
+
*/
|
|
97
|
+
function main() {
|
|
98
|
+
// Try platform package first, then fallback to downloaded binary
|
|
99
|
+
let binaryPath = findBinaryFromPackage() || findDownloadedBinary();
|
|
100
|
+
|
|
101
|
+
if (!binaryPath) {
|
|
102
|
+
console.error('Error: tool-hub-mcp binary not found.');
|
|
103
|
+
console.error('This may happen if:');
|
|
104
|
+
console.error(' 1. optionalDependencies were disabled during install');
|
|
105
|
+
console.error(' 2. postinstall script failed to download the binary');
|
|
106
|
+
console.error('');
|
|
107
|
+
console.error('Try reinstalling: npm install @khanglvm/tool-hub-mcp');
|
|
108
|
+
console.error('Or download manually from: https://github.com/khanglvm/tool-hub-mcp/releases');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Spawn the binary with all arguments passed through
|
|
113
|
+
const child = spawn(binaryPath, process.argv.slice(2), {
|
|
114
|
+
stdio: 'inherit', // Pass through stdin/stdout/stderr
|
|
115
|
+
shell: false
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Forward exit code
|
|
119
|
+
child.on('close', (code) => {
|
|
120
|
+
process.exit(code ?? 0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Handle spawn errors
|
|
124
|
+
child.on('error', (err) => {
|
|
125
|
+
console.error(`Failed to start tool-hub-mcp: ${err.message}`);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@khanglvm/tool-hub-mcp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Serverless MCP Aggregator - Reduce AI context token consumption by 60-97%",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tool-hub-mcp": "./cli.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "./cli.js",
|
|
9
|
+
"files": [
|
|
10
|
+
"cli.js",
|
|
11
|
+
"postinstall.js"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"postinstall": "node postinstall.js"
|
|
15
|
+
},
|
|
16
|
+
"optionalDependencies": {
|
|
17
|
+
"@khanglvm/tool-hub-mcp-darwin-arm64": "1.0.0",
|
|
18
|
+
"@khanglvm/tool-hub-mcp-darwin-x64": "1.0.0",
|
|
19
|
+
"@khanglvm/tool-hub-mcp-linux-x64": "1.0.0",
|
|
20
|
+
"@khanglvm/tool-hub-mcp-linux-arm64": "1.0.0",
|
|
21
|
+
"@khanglvm/tool-hub-mcp-win32-x64": "1.0.0"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"mcp",
|
|
25
|
+
"model-context-protocol",
|
|
26
|
+
"ai",
|
|
27
|
+
"claude",
|
|
28
|
+
"opencode",
|
|
29
|
+
"aggregator",
|
|
30
|
+
"serverless"
|
|
31
|
+
],
|
|
32
|
+
"author": "khanglvm",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/khanglvm/tool-hub-mcp.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/khanglvm/tool-hub-mcp/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/khanglvm/tool-hub-mcp#readme",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=18"
|
|
44
|
+
}
|
|
45
|
+
}
|
package/postinstall.js
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Postinstall fallback for tool-hub-mcp.
|
|
5
|
+
* Downloads the Go binary from GitHub Releases if optionalDependencies were not installed.
|
|
6
|
+
*
|
|
7
|
+
* This handles environments where:
|
|
8
|
+
* - optionalDependencies are disabled (some CI environments)
|
|
9
|
+
* - Package manager fails to resolve platform-specific packages
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const https = require('https');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const { execSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
// Configuration
|
|
19
|
+
const GITHUB_REPO = 'khanglvm/tool-hub-mcp';
|
|
20
|
+
const BINARY_NAME = 'tool-hub-mcp';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get platform suffix for GitHub release assets.
|
|
24
|
+
* @returns {string} Suffix like 'Darwin-arm64' or 'Linux-x86_64'
|
|
25
|
+
*/
|
|
26
|
+
function getPlatformSuffix() {
|
|
27
|
+
const platform = os.platform();
|
|
28
|
+
const arch = os.arch();
|
|
29
|
+
|
|
30
|
+
const platformMap = {
|
|
31
|
+
'darwin': 'Darwin',
|
|
32
|
+
'linux': 'Linux',
|
|
33
|
+
'win32': 'Windows'
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const archMap = {
|
|
37
|
+
'arm64': 'arm64',
|
|
38
|
+
'x64': 'x86_64'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const mappedPlatform = platformMap[platform];
|
|
42
|
+
const mappedArch = archMap[arch];
|
|
43
|
+
|
|
44
|
+
if (!mappedPlatform || !mappedArch) {
|
|
45
|
+
throw new Error(`Unsupported platform: ${platform}-${arch}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const suffix = `${mappedPlatform}-${mappedArch}`;
|
|
49
|
+
return platform === 'win32' ? `${suffix}.exe` : suffix;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Check if platform package binary already exists.
|
|
54
|
+
* @returns {boolean} True if binary found in optionalDependencies
|
|
55
|
+
*/
|
|
56
|
+
function hasPlatformBinary() {
|
|
57
|
+
const platform = os.platform();
|
|
58
|
+
const arch = os.arch();
|
|
59
|
+
|
|
60
|
+
const archMap = { 'arm64': 'arm64', 'x64': 'x64' };
|
|
61
|
+
const platformMap = { 'darwin': 'darwin', 'linux': 'linux', 'win32': 'win32' };
|
|
62
|
+
|
|
63
|
+
const packageName = `@khanglvm/tool-hub-mcp-${platformMap[platform]}-${archMap[arch]}`;
|
|
64
|
+
const binaryName = platform === 'win32' ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
65
|
+
|
|
66
|
+
const searchPaths = [
|
|
67
|
+
path.join(__dirname, 'node_modules', packageName, 'bin', binaryName),
|
|
68
|
+
path.join(__dirname, '..', packageName, 'bin', binaryName),
|
|
69
|
+
path.join(__dirname, '..', '..', packageName, 'bin', binaryName),
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
return searchPaths.some(p => fs.existsSync(p));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Follow redirects and download file.
|
|
77
|
+
* @param {string} url - URL to download
|
|
78
|
+
* @param {string} dest - Destination file path
|
|
79
|
+
* @returns {Promise<void>}
|
|
80
|
+
*/
|
|
81
|
+
function downloadFile(url, dest) {
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
const file = fs.createWriteStream(dest);
|
|
84
|
+
|
|
85
|
+
const request = (url) => {
|
|
86
|
+
https.get(url, (response) => {
|
|
87
|
+
// Handle redirects (GitHub releases use 302)
|
|
88
|
+
if (response.statusCode === 301 || response.statusCode === 302) {
|
|
89
|
+
file.close();
|
|
90
|
+
fs.unlinkSync(dest);
|
|
91
|
+
request(response.headers.location);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (response.statusCode !== 200) {
|
|
96
|
+
file.close();
|
|
97
|
+
fs.unlinkSync(dest);
|
|
98
|
+
reject(new Error(`HTTP ${response.statusCode}: Failed to download ${url}`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
response.pipe(file);
|
|
103
|
+
file.on('finish', () => {
|
|
104
|
+
file.close();
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
}).on('error', (err) => {
|
|
108
|
+
file.close();
|
|
109
|
+
fs.unlinkSync(dest);
|
|
110
|
+
reject(err);
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
request(url);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get the latest release version from GitHub API.
|
|
120
|
+
* @returns {Promise<string>} Version tag like 'v1.0.0'
|
|
121
|
+
*/
|
|
122
|
+
function getLatestVersion() {
|
|
123
|
+
return new Promise((resolve, reject) => {
|
|
124
|
+
const options = {
|
|
125
|
+
hostname: 'api.github.com',
|
|
126
|
+
path: `/repos/${GITHUB_REPO}/releases/latest`,
|
|
127
|
+
headers: { 'User-Agent': 'tool-hub-mcp-installer' }
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
https.get(options, (response) => {
|
|
131
|
+
let data = '';
|
|
132
|
+
response.on('data', chunk => data += chunk);
|
|
133
|
+
response.on('end', () => {
|
|
134
|
+
try {
|
|
135
|
+
const release = JSON.parse(data);
|
|
136
|
+
if (release.tag_name) {
|
|
137
|
+
resolve(release.tag_name);
|
|
138
|
+
} else {
|
|
139
|
+
reject(new Error('No releases found'));
|
|
140
|
+
}
|
|
141
|
+
} catch (e) {
|
|
142
|
+
reject(e);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
}).on('error', reject);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Main postinstall logic.
|
|
151
|
+
*/
|
|
152
|
+
async function main() {
|
|
153
|
+
// Skip if platform binary already installed via optionalDependencies
|
|
154
|
+
if (hasPlatformBinary()) {
|
|
155
|
+
console.log('tool-hub-mcp: Binary found from optionalDependencies ✓');
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
console.log('tool-hub-mcp: optionalDependencies not available, downloading from GitHub...');
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// Get latest version
|
|
163
|
+
const version = await getLatestVersion();
|
|
164
|
+
console.log(`tool-hub-mcp: Downloading version ${version}...`);
|
|
165
|
+
|
|
166
|
+
// Prepare download
|
|
167
|
+
const suffix = getPlatformSuffix();
|
|
168
|
+
const binaryName = os.platform() === 'win32' ? `${BINARY_NAME}.exe` : BINARY_NAME;
|
|
169
|
+
const downloadUrl = `https://github.com/${GITHUB_REPO}/releases/download/${version}/${BINARY_NAME}-${suffix}`;
|
|
170
|
+
|
|
171
|
+
// Create bin directory
|
|
172
|
+
const binDir = path.join(__dirname, 'bin');
|
|
173
|
+
if (!fs.existsSync(binDir)) {
|
|
174
|
+
fs.mkdirSync(binDir, { recursive: true });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const destPath = path.join(binDir, binaryName);
|
|
178
|
+
|
|
179
|
+
// Download binary
|
|
180
|
+
await downloadFile(downloadUrl, destPath);
|
|
181
|
+
|
|
182
|
+
// Make executable (Unix only)
|
|
183
|
+
if (os.platform() !== 'win32') {
|
|
184
|
+
fs.chmodSync(destPath, 0o755);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log('tool-hub-mcp: Binary downloaded and installed ✓');
|
|
188
|
+
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`tool-hub-mcp: Failed to download binary: ${error.message}`);
|
|
191
|
+
console.error('You can manually download from: https://github.com/khanglvm/tool-hub-mcp/releases');
|
|
192
|
+
// Don't fail the install - user can still manually download
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
main();
|