@nimblebrain/mpak 0.0.2 → 0.2.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/LICENSE +10 -198
- package/README.md +50 -360
- package/dist/index.d.ts +0 -2
- package/dist/index.js +2113 -4
- package/dist/index.js.map +1 -1
- package/package.json +32 -29
- package/.claude/settings.local.json +0 -19
- package/.env.example +0 -13
- package/.github/workflows/ci.yml +0 -27
- package/CLAUDE.md +0 -271
- package/dist/commands/config.d.ts +0 -31
- package/dist/commands/config.d.ts.map +0 -1
- package/dist/commands/config.js +0 -129
- package/dist/commands/config.js.map +0 -1
- package/dist/commands/packages/pull.d.ts +0 -11
- package/dist/commands/packages/pull.d.ts.map +0 -1
- package/dist/commands/packages/pull.js +0 -72
- package/dist/commands/packages/pull.js.map +0 -1
- package/dist/commands/packages/run.d.ts +0 -36
- package/dist/commands/packages/run.d.ts.map +0 -1
- package/dist/commands/packages/run.js +0 -348
- package/dist/commands/packages/run.js.map +0 -1
- package/dist/commands/packages/search.d.ts +0 -12
- package/dist/commands/packages/search.d.ts.map +0 -1
- package/dist/commands/packages/search.js +0 -63
- package/dist/commands/packages/search.js.map +0 -1
- package/dist/commands/packages/show.d.ts +0 -8
- package/dist/commands/packages/show.d.ts.map +0 -1
- package/dist/commands/packages/show.js +0 -109
- package/dist/commands/packages/show.js.map +0 -1
- package/dist/commands/search.d.ts +0 -12
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/search.js +0 -144
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/skills/index.d.ts +0 -8
- package/dist/commands/skills/index.d.ts.map +0 -1
- package/dist/commands/skills/index.js +0 -8
- package/dist/commands/skills/index.js.map +0 -1
- package/dist/commands/skills/install.d.ts +0 -9
- package/dist/commands/skills/install.d.ts.map +0 -1
- package/dist/commands/skills/install.js +0 -110
- package/dist/commands/skills/install.js.map +0 -1
- package/dist/commands/skills/list.d.ts +0 -8
- package/dist/commands/skills/list.d.ts.map +0 -1
- package/dist/commands/skills/list.js +0 -89
- package/dist/commands/skills/list.js.map +0 -1
- package/dist/commands/skills/pack.d.ts +0 -22
- package/dist/commands/skills/pack.d.ts.map +0 -1
- package/dist/commands/skills/pack.js +0 -116
- package/dist/commands/skills/pack.js.map +0 -1
- package/dist/commands/skills/pull.d.ts +0 -9
- package/dist/commands/skills/pull.d.ts.map +0 -1
- package/dist/commands/skills/pull.js +0 -68
- package/dist/commands/skills/pull.js.map +0 -1
- package/dist/commands/skills/search.d.ts +0 -14
- package/dist/commands/skills/search.d.ts.map +0 -1
- package/dist/commands/skills/search.js +0 -53
- package/dist/commands/skills/search.js.map +0 -1
- package/dist/commands/skills/show.d.ts +0 -8
- package/dist/commands/skills/show.d.ts.map +0 -1
- package/dist/commands/skills/show.js +0 -64
- package/dist/commands/skills/show.js.map +0 -1
- package/dist/commands/skills/validate.d.ts +0 -25
- package/dist/commands/skills/validate.d.ts.map +0 -1
- package/dist/commands/skills/validate.js +0 -191
- package/dist/commands/skills/validate.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/lib/api/registry-client.d.ts +0 -63
- package/dist/lib/api/registry-client.d.ts.map +0 -1
- package/dist/lib/api/registry-client.js +0 -167
- package/dist/lib/api/registry-client.js.map +0 -1
- package/dist/lib/api/skills-client.d.ts +0 -30
- package/dist/lib/api/skills-client.d.ts.map +0 -1
- package/dist/lib/api/skills-client.js +0 -110
- package/dist/lib/api/skills-client.js.map +0 -1
- package/dist/program.d.ts +0 -12
- package/dist/program.d.ts.map +0 -1
- package/dist/program.js +0 -174
- package/dist/program.js.map +0 -1
- package/dist/schemas/generated/api-responses.d.ts +0 -541
- package/dist/schemas/generated/api-responses.d.ts.map +0 -1
- package/dist/schemas/generated/api-responses.js +0 -313
- package/dist/schemas/generated/api-responses.js.map +0 -1
- package/dist/schemas/generated/auth.d.ts +0 -18
- package/dist/schemas/generated/auth.d.ts.map +0 -1
- package/dist/schemas/generated/auth.js +0 -18
- package/dist/schemas/generated/auth.js.map +0 -1
- package/dist/schemas/generated/index.d.ts +0 -5
- package/dist/schemas/generated/index.d.ts.map +0 -1
- package/dist/schemas/generated/index.js +0 -6
- package/dist/schemas/generated/index.js.map +0 -1
- package/dist/schemas/generated/package.d.ts +0 -43
- package/dist/schemas/generated/package.d.ts.map +0 -1
- package/dist/schemas/generated/package.js +0 -20
- package/dist/schemas/generated/package.js.map +0 -1
- package/dist/schemas/generated/skill.d.ts +0 -381
- package/dist/schemas/generated/skill.d.ts.map +0 -1
- package/dist/schemas/generated/skill.js +0 -216
- package/dist/schemas/generated/skill.js.map +0 -1
- package/dist/utils/config-manager.d.ts +0 -66
- package/dist/utils/config-manager.d.ts.map +0 -1
- package/dist/utils/config-manager.js +0 -193
- package/dist/utils/config-manager.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -12
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -27
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/version.d.ts +0 -5
- package/dist/utils/version.d.ts.map +0 -1
- package/dist/utils/version.js +0 -19
- package/dist/utils/version.js.map +0 -1
- package/eslint.config.js +0 -63
- package/src/commands/config.ts +0 -162
- package/src/commands/packages/pull.ts +0 -96
- package/src/commands/packages/run.test.ts +0 -222
- package/src/commands/packages/run.ts +0 -451
- package/src/commands/packages/search.ts +0 -83
- package/src/commands/packages/show.ts +0 -128
- package/src/commands/search.ts +0 -191
- package/src/commands/skills/index.ts +0 -7
- package/src/commands/skills/install.ts +0 -129
- package/src/commands/skills/list.ts +0 -116
- package/src/commands/skills/pack.test.ts +0 -260
- package/src/commands/skills/pack.ts +0 -145
- package/src/commands/skills/pull.ts +0 -88
- package/src/commands/skills/search.ts +0 -73
- package/src/commands/skills/show.ts +0 -72
- package/src/commands/skills/validate.test.ts +0 -466
- package/src/commands/skills/validate.ts +0 -227
- package/src/index.ts +0 -11
- package/src/lib/api/registry-client.ts +0 -223
- package/src/lib/api/schema.d.ts +0 -520
- package/src/lib/api/skills-client.ts +0 -148
- package/src/program.test.ts +0 -22
- package/src/program.ts +0 -212
- package/src/schemas/config.v1.schema.json +0 -37
- package/src/schemas/generated/api-responses.ts +0 -386
- package/src/schemas/generated/auth.ts +0 -21
- package/src/schemas/generated/index.ts +0 -5
- package/src/schemas/generated/package.ts +0 -29
- package/src/schemas/generated/skill.ts +0 -271
- package/src/utils/config-manager.test.ts +0 -330
- package/src/utils/config-manager.ts +0 -272
- package/src/utils/errors.test.ts +0 -25
- package/src/utils/errors.ts +0 -33
- package/src/utils/version.test.ts +0 -16
- package/src/utils/version.ts +0 -18
- package/test/integration/registry-client.test.ts +0 -180
- package/tsconfig.check.json +0 -9
- package/tsconfig.json +0 -25
- package/vitest.config.ts +0 -14
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { homedir } from 'os';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import { parsePackageSpec, getCacheDir, resolveArgs, substituteUserConfig, substituteEnvVars } from './run.js';
|
|
5
|
-
|
|
6
|
-
describe('parsePackageSpec', () => {
|
|
7
|
-
describe('scoped packages', () => {
|
|
8
|
-
it('parses @scope/name without version', () => {
|
|
9
|
-
expect(parsePackageSpec('@scope/name')).toEqual({
|
|
10
|
-
name: '@scope/name',
|
|
11
|
-
});
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('parses @scope/name@1.0.0', () => {
|
|
15
|
-
expect(parsePackageSpec('@scope/name@1.0.0')).toEqual({
|
|
16
|
-
name: '@scope/name',
|
|
17
|
-
version: '1.0.0',
|
|
18
|
-
});
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('parses prerelease versions @scope/name@1.0.0-beta.1', () => {
|
|
22
|
-
expect(parsePackageSpec('@scope/name@1.0.0-beta.1')).toEqual({
|
|
23
|
-
name: '@scope/name',
|
|
24
|
-
version: '1.0.0-beta.1',
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('parses version with build metadata @scope/name@1.0.0+build.123', () => {
|
|
29
|
-
expect(parsePackageSpec('@scope/name@1.0.0+build.123')).toEqual({
|
|
30
|
-
name: '@scope/name',
|
|
31
|
-
version: '1.0.0+build.123',
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
describe('edge cases', () => {
|
|
37
|
-
it('handles package name with multiple slashes @org/sub/name', () => {
|
|
38
|
-
// This is technically invalid per npm spec, but we should handle gracefully
|
|
39
|
-
const result = parsePackageSpec('@org/sub/name');
|
|
40
|
-
expect(result.name).toBe('@org/sub/name');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('handles unscoped package name', () => {
|
|
44
|
-
expect(parsePackageSpec('simple-name')).toEqual({
|
|
45
|
-
name: 'simple-name',
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('treats unscoped@version as invalid (mpak requires scoped packages)', () => {
|
|
50
|
-
// mpak only supports scoped packages (@scope/name)
|
|
51
|
-
// An unscoped name with @ is treated as the full name, not name@version
|
|
52
|
-
expect(parsePackageSpec('unscoped@1.0.0')).toEqual({
|
|
53
|
-
name: 'unscoped@1.0.0',
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('handles empty string', () => {
|
|
58
|
-
expect(parsePackageSpec('')).toEqual({ name: '' });
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('handles @ only', () => {
|
|
62
|
-
expect(parsePackageSpec('@')).toEqual({ name: '@' });
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('getCacheDir', () => {
|
|
68
|
-
const expectedBase = join(homedir(), '.mpak', 'cache');
|
|
69
|
-
|
|
70
|
-
it('converts @scope/name to scope-name', () => {
|
|
71
|
-
expect(getCacheDir('@nimblebraininc/echo')).toBe(
|
|
72
|
-
join(expectedBase, 'nimblebraininc-echo')
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('handles simple scoped names', () => {
|
|
77
|
-
expect(getCacheDir('@foo/bar')).toBe(join(expectedBase, 'foo-bar'));
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('handles unscoped names', () => {
|
|
81
|
-
expect(getCacheDir('simple')).toBe(join(expectedBase, 'simple'));
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('resolveArgs', () => {
|
|
86
|
-
const cacheDir = '/Users/test/.mpak/cache/scope-name';
|
|
87
|
-
|
|
88
|
-
it('resolves ${__dirname} placeholder', () => {
|
|
89
|
-
expect(resolveArgs(['${__dirname}/dist/index.js'], cacheDir)).toEqual([
|
|
90
|
-
`${cacheDir}/dist/index.js`,
|
|
91
|
-
]);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('resolves multiple ${__dirname} in single arg', () => {
|
|
95
|
-
expect(
|
|
96
|
-
resolveArgs(['--config=${__dirname}/config.json'], cacheDir)
|
|
97
|
-
).toEqual([`--config=${cacheDir}/config.json`]);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('resolves ${__dirname} in multiple args', () => {
|
|
101
|
-
expect(
|
|
102
|
-
resolveArgs(
|
|
103
|
-
['${__dirname}/index.js', '--config', '${__dirname}/config.json'],
|
|
104
|
-
cacheDir
|
|
105
|
-
)
|
|
106
|
-
).toEqual([
|
|
107
|
-
`${cacheDir}/index.js`,
|
|
108
|
-
'--config',
|
|
109
|
-
`${cacheDir}/config.json`,
|
|
110
|
-
]);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('leaves args without placeholders unchanged', () => {
|
|
114
|
-
expect(resolveArgs(['-m', 'mcp_echo.server'], cacheDir)).toEqual([
|
|
115
|
-
'-m',
|
|
116
|
-
'mcp_echo.server',
|
|
117
|
-
]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
it('handles empty args array', () => {
|
|
121
|
-
expect(resolveArgs([], cacheDir)).toEqual([]);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('handles Windows-style paths in cacheDir', () => {
|
|
125
|
-
const winPath = 'C:\\Users\\test\\.mpak\\cache\\scope-name';
|
|
126
|
-
expect(resolveArgs(['${__dirname}\\dist\\index.js'], winPath)).toEqual([
|
|
127
|
-
`${winPath}\\dist\\index.js`,
|
|
128
|
-
]);
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe('substituteUserConfig', () => {
|
|
133
|
-
it('substitutes single user_config variable', () => {
|
|
134
|
-
expect(
|
|
135
|
-
substituteUserConfig('${user_config.api_key}', { api_key: 'secret123' })
|
|
136
|
-
).toBe('secret123');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('substitutes multiple user_config variables', () => {
|
|
140
|
-
expect(
|
|
141
|
-
substituteUserConfig('key=${user_config.key}&secret=${user_config.secret}', {
|
|
142
|
-
key: 'mykey',
|
|
143
|
-
secret: 'mysecret',
|
|
144
|
-
})
|
|
145
|
-
).toBe('key=mykey&secret=mysecret');
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('leaves unmatched variables unchanged', () => {
|
|
149
|
-
expect(
|
|
150
|
-
substituteUserConfig('${user_config.missing}', { other: 'value' })
|
|
151
|
-
).toBe('${user_config.missing}');
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it('handles mixed matched and unmatched variables', () => {
|
|
155
|
-
expect(
|
|
156
|
-
substituteUserConfig('${user_config.found}-${user_config.missing}', {
|
|
157
|
-
found: 'yes',
|
|
158
|
-
})
|
|
159
|
-
).toBe('yes-${user_config.missing}');
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('handles empty config values', () => {
|
|
163
|
-
expect(
|
|
164
|
-
substituteUserConfig('${user_config.empty}', { empty: '' })
|
|
165
|
-
).toBe('');
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('handles values with special characters', () => {
|
|
169
|
-
expect(
|
|
170
|
-
substituteUserConfig('${user_config.key}', { key: 'abc$def{ghi}' })
|
|
171
|
-
).toBe('abc$def{ghi}');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('leaves non-user_config placeholders unchanged', () => {
|
|
175
|
-
expect(
|
|
176
|
-
substituteUserConfig('${__dirname}/path', { dirname: '/cache' })
|
|
177
|
-
).toBe('${__dirname}/path');
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe('substituteEnvVars', () => {
|
|
182
|
-
it('substitutes user_config in all env vars', () => {
|
|
183
|
-
const env = {
|
|
184
|
-
API_KEY: '${user_config.api_key}',
|
|
185
|
-
DEBUG: 'true',
|
|
186
|
-
TOKEN: '${user_config.token}',
|
|
187
|
-
};
|
|
188
|
-
const values = { api_key: 'key123', token: 'tok456' };
|
|
189
|
-
|
|
190
|
-
expect(substituteEnvVars(env, values)).toEqual({
|
|
191
|
-
API_KEY: 'key123',
|
|
192
|
-
DEBUG: 'true',
|
|
193
|
-
TOKEN: 'tok456',
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
it('handles undefined env', () => {
|
|
198
|
-
expect(substituteEnvVars(undefined, { key: 'value' })).toEqual({});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('handles empty env', () => {
|
|
202
|
-
expect(substituteEnvVars({}, { key: 'value' })).toEqual({});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
it('preserves env vars without placeholders', () => {
|
|
206
|
-
const env = { PATH: '/usr/bin', HOME: '/home/user' };
|
|
207
|
-
expect(substituteEnvVars(env, {})).toEqual(env);
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('leaves unsubstituted placeholders as-is', () => {
|
|
211
|
-
const env = {
|
|
212
|
-
API_KEY: '${user_config.api_key}',
|
|
213
|
-
DEBUG: 'true',
|
|
214
|
-
};
|
|
215
|
-
// api_key not provided, so placeholder remains
|
|
216
|
-
// (process.env will override this at merge time)
|
|
217
|
-
expect(substituteEnvVars(env, {})).toEqual({
|
|
218
|
-
API_KEY: '${user_config.api_key}',
|
|
219
|
-
DEBUG: 'true',
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
});
|
|
@@ -1,451 +0,0 @@
|
|
|
1
|
-
import { spawn, spawnSync } from 'child_process';
|
|
2
|
-
import { createInterface } from 'readline';
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from 'fs';
|
|
4
|
-
import { homedir } from 'os';
|
|
5
|
-
import { join, dirname } from 'path';
|
|
6
|
-
import { RegistryClient } from '../../lib/api/registry-client.js';
|
|
7
|
-
import { ConfigManager } from '../../utils/config-manager.js';
|
|
8
|
-
|
|
9
|
-
export interface RunOptions {
|
|
10
|
-
update?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface McpConfig {
|
|
14
|
-
command: string;
|
|
15
|
-
args: string[];
|
|
16
|
-
env?: Record<string, string>;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* User configuration field definition (MCPB v0.3 spec)
|
|
21
|
-
*/
|
|
22
|
-
interface UserConfigField {
|
|
23
|
-
type: 'string' | 'number' | 'boolean';
|
|
24
|
-
title?: string;
|
|
25
|
-
description?: string;
|
|
26
|
-
sensitive?: boolean;
|
|
27
|
-
required?: boolean;
|
|
28
|
-
default?: string | number | boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface McpbManifest {
|
|
32
|
-
manifest_version: string;
|
|
33
|
-
name: string;
|
|
34
|
-
version: string;
|
|
35
|
-
description: string;
|
|
36
|
-
user_config?: Record<string, UserConfigField>;
|
|
37
|
-
server: {
|
|
38
|
-
type: 'node' | 'python' | 'binary';
|
|
39
|
-
entry_point: string;
|
|
40
|
-
mcp_config: McpConfig;
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
interface CacheMetadata {
|
|
45
|
-
version: string;
|
|
46
|
-
pulledAt: string;
|
|
47
|
-
platform: { os: string; arch: string };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Parse package specification into name and version
|
|
52
|
-
* @example parsePackageSpec('@scope/name') => { name: '@scope/name' }
|
|
53
|
-
* @example parsePackageSpec('@scope/name@1.0.0') => { name: '@scope/name', version: '1.0.0' }
|
|
54
|
-
*/
|
|
55
|
-
export function parsePackageSpec(spec: string): { name: string; version?: string } {
|
|
56
|
-
const lastAtIndex = spec.lastIndexOf('@');
|
|
57
|
-
|
|
58
|
-
if (lastAtIndex <= 0) {
|
|
59
|
-
return { name: spec };
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const name = spec.substring(0, lastAtIndex);
|
|
63
|
-
const version = spec.substring(lastAtIndex + 1);
|
|
64
|
-
|
|
65
|
-
if (!name.startsWith('@')) {
|
|
66
|
-
return { name: spec };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return { name, version };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Get cache directory for a package
|
|
74
|
-
* @example getCacheDir('@scope/name') => '~/.mpak/cache/scope-name'
|
|
75
|
-
*/
|
|
76
|
-
export function getCacheDir(packageName: string): string {
|
|
77
|
-
const cacheBase = join(homedir(), '.mpak', 'cache');
|
|
78
|
-
// @scope/name -> scope/name
|
|
79
|
-
const safeName = packageName.replace('@', '').replace('/', '-');
|
|
80
|
-
return join(cacheBase, safeName);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Read cache metadata
|
|
85
|
-
*/
|
|
86
|
-
function getCacheMetadata(cacheDir: string): CacheMetadata | null {
|
|
87
|
-
const metaPath = join(cacheDir, '.mpak-meta.json');
|
|
88
|
-
if (!existsSync(metaPath)) {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
try {
|
|
92
|
-
return JSON.parse(readFileSync(metaPath, 'utf8'));
|
|
93
|
-
} catch {
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Write cache metadata
|
|
100
|
-
*/
|
|
101
|
-
function writeCacheMetadata(cacheDir: string, metadata: CacheMetadata): void {
|
|
102
|
-
const metaPath = join(cacheDir, '.mpak-meta.json');
|
|
103
|
-
writeFileSync(metaPath, JSON.stringify(metadata, null, 2));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Extract ZIP file to directory (simple implementation without external deps)
|
|
108
|
-
*/
|
|
109
|
-
async function extractZip(zipPath: string, destDir: string): Promise<void> {
|
|
110
|
-
// Use native unzip command (available on macOS, Linux, and Windows with WSL)
|
|
111
|
-
const { execSync } = await import('child_process');
|
|
112
|
-
|
|
113
|
-
// Ensure destination exists
|
|
114
|
-
mkdirSync(destDir, { recursive: true });
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
execSync(`unzip -o -q "${zipPath}" -d "${destDir}"`, { stdio: 'pipe' });
|
|
118
|
-
} catch (error: any) {
|
|
119
|
-
throw new Error(`Failed to extract bundle: ${error.message}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Read manifest from extracted bundle
|
|
125
|
-
*/
|
|
126
|
-
function readManifest(cacheDir: string): McpbManifest {
|
|
127
|
-
const manifestPath = join(cacheDir, 'manifest.json');
|
|
128
|
-
if (!existsSync(manifestPath)) {
|
|
129
|
-
throw new Error(`Manifest not found in bundle: ${manifestPath}`);
|
|
130
|
-
}
|
|
131
|
-
return JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Resolve placeholders in args (e.g., ${__dirname})
|
|
136
|
-
* @example resolveArgs(['${__dirname}/index.js'], '/cache') => ['/cache/index.js']
|
|
137
|
-
*/
|
|
138
|
-
export function resolveArgs(args: string[], cacheDir: string): string[] {
|
|
139
|
-
return args.map(arg =>
|
|
140
|
-
arg.replace(/\$\{__dirname\}/g, cacheDir)
|
|
141
|
-
);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Substitute ${user_config.*} placeholders in a string
|
|
146
|
-
* @example substituteUserConfig('${user_config.api_key}', { api_key: 'secret' }) => 'secret'
|
|
147
|
-
*/
|
|
148
|
-
export function substituteUserConfig(
|
|
149
|
-
value: string,
|
|
150
|
-
userConfigValues: Record<string, string>
|
|
151
|
-
): string {
|
|
152
|
-
return value.replace(/\$\{user_config\.([^}]+)\}/g, (match, key) => {
|
|
153
|
-
return userConfigValues[key] ?? match;
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Substitute ${user_config.*} placeholders in env vars
|
|
159
|
-
*/
|
|
160
|
-
export function substituteEnvVars(
|
|
161
|
-
env: Record<string, string> | undefined,
|
|
162
|
-
userConfigValues: Record<string, string>
|
|
163
|
-
): Record<string, string> {
|
|
164
|
-
if (!env) return {};
|
|
165
|
-
const result: Record<string, string> = {};
|
|
166
|
-
for (const [key, value] of Object.entries(env)) {
|
|
167
|
-
result[key] = substituteUserConfig(value, userConfigValues);
|
|
168
|
-
}
|
|
169
|
-
return result;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Prompt user for a config value (interactive terminal input)
|
|
174
|
-
*/
|
|
175
|
-
async function promptForValue(
|
|
176
|
-
field: UserConfigField,
|
|
177
|
-
key: string
|
|
178
|
-
): Promise<string> {
|
|
179
|
-
return new Promise((resolve) => {
|
|
180
|
-
const rl = createInterface({
|
|
181
|
-
input: process.stdin,
|
|
182
|
-
output: process.stderr,
|
|
183
|
-
terminal: true,
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const label = field.title || key;
|
|
187
|
-
const hint = field.description ? ` (${field.description})` : '';
|
|
188
|
-
const defaultHint = field.default !== undefined ? ` [${field.default}]` : '';
|
|
189
|
-
const prompt = `=> ${label}${hint}${defaultHint}: `;
|
|
190
|
-
|
|
191
|
-
// For sensitive fields, we'd ideally hide input, but Node's readline
|
|
192
|
-
// doesn't support this natively. We'll just note it's sensitive.
|
|
193
|
-
if (field.sensitive) {
|
|
194
|
-
process.stderr.write(`=> (sensitive input)\n`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
rl.question(prompt, (answer) => {
|
|
198
|
-
rl.close();
|
|
199
|
-
// Use default if empty and default exists
|
|
200
|
-
if (!answer && field.default !== undefined) {
|
|
201
|
-
resolve(String(field.default));
|
|
202
|
-
} else {
|
|
203
|
-
resolve(answer);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Check if we're in an interactive terminal
|
|
211
|
-
*/
|
|
212
|
-
function isInteractive(): boolean {
|
|
213
|
-
return process.stdin.isTTY === true;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Gather user config values from stored config
|
|
218
|
-
* Prompts for missing required values if interactive
|
|
219
|
-
*/
|
|
220
|
-
async function gatherUserConfigValues(
|
|
221
|
-
packageName: string,
|
|
222
|
-
userConfig: Record<string, UserConfigField>,
|
|
223
|
-
configManager: ConfigManager
|
|
224
|
-
): Promise<Record<string, string>> {
|
|
225
|
-
const result: Record<string, string> = {};
|
|
226
|
-
const storedConfig = configManager.getPackageConfig(packageName) || {};
|
|
227
|
-
const missingRequired: Array<{ key: string; field: UserConfigField }> = [];
|
|
228
|
-
|
|
229
|
-
for (const [key, field] of Object.entries(userConfig)) {
|
|
230
|
-
// Priority: 1) stored config, 2) default value
|
|
231
|
-
const storedValue = storedConfig[key];
|
|
232
|
-
|
|
233
|
-
if (storedValue !== undefined) {
|
|
234
|
-
result[key] = storedValue;
|
|
235
|
-
} else if (field.default !== undefined) {
|
|
236
|
-
result[key] = String(field.default);
|
|
237
|
-
} else if (field.required) {
|
|
238
|
-
missingRequired.push({ key, field });
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Prompt for missing required values if interactive
|
|
243
|
-
if (missingRequired.length > 0) {
|
|
244
|
-
if (!isInteractive()) {
|
|
245
|
-
const missingKeys = missingRequired.map(m => m.key).join(', ');
|
|
246
|
-
process.stderr.write(`=> Error: Missing required config: ${missingKeys}\n`);
|
|
247
|
-
process.stderr.write(`=> Run 'mpak config set ${packageName} <key>=<value>' to set values\n`);
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
process.stderr.write(`=> Package requires configuration:\n`);
|
|
252
|
-
for (const { key, field } of missingRequired) {
|
|
253
|
-
const value = await promptForValue(field, key);
|
|
254
|
-
if (!value && field.required) {
|
|
255
|
-
process.stderr.write(`=> Error: ${field.title || key} is required\n`);
|
|
256
|
-
process.exit(1);
|
|
257
|
-
}
|
|
258
|
-
result[key] = value;
|
|
259
|
-
|
|
260
|
-
// Offer to save the value
|
|
261
|
-
if (value) {
|
|
262
|
-
const rl = createInterface({
|
|
263
|
-
input: process.stdin,
|
|
264
|
-
output: process.stderr,
|
|
265
|
-
terminal: true,
|
|
266
|
-
});
|
|
267
|
-
await new Promise<void>((resolve) => {
|
|
268
|
-
rl.question(`=> Save ${field.title || key} for future runs? [Y/n]: `, (answer) => {
|
|
269
|
-
rl.close();
|
|
270
|
-
if (answer.toLowerCase() !== 'n') {
|
|
271
|
-
configManager.setPackageConfigValue(packageName, key, value);
|
|
272
|
-
process.stderr.write(`=> Saved to ~/.mpak/config.json\n`);
|
|
273
|
-
}
|
|
274
|
-
resolve();
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
return result;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/**
|
|
285
|
-
* Find Python executable (tries python3 first, then python)
|
|
286
|
-
*/
|
|
287
|
-
function findPythonCommand(): string {
|
|
288
|
-
// Try python3 first (preferred on macOS/Linux)
|
|
289
|
-
const result = spawnSync('python3', ['--version'], { stdio: 'pipe' });
|
|
290
|
-
if (result.status === 0) {
|
|
291
|
-
return 'python3';
|
|
292
|
-
}
|
|
293
|
-
// Fall back to python
|
|
294
|
-
return 'python';
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/**
|
|
298
|
-
* Run a package from the registry
|
|
299
|
-
*/
|
|
300
|
-
export async function handleRun(
|
|
301
|
-
packageSpec: string,
|
|
302
|
-
options: RunOptions = {}
|
|
303
|
-
): Promise<void> {
|
|
304
|
-
const { name, version: requestedVersion } = parsePackageSpec(packageSpec);
|
|
305
|
-
const client = new RegistryClient();
|
|
306
|
-
const platform = RegistryClient.detectPlatform();
|
|
307
|
-
const cacheDir = getCacheDir(name);
|
|
308
|
-
|
|
309
|
-
let needsPull = true;
|
|
310
|
-
let cachedMeta = getCacheMetadata(cacheDir);
|
|
311
|
-
|
|
312
|
-
// Check if we have a cached version
|
|
313
|
-
if (cachedMeta && !options.update) {
|
|
314
|
-
if (requestedVersion) {
|
|
315
|
-
// Specific version requested - check if cached version matches
|
|
316
|
-
needsPull = cachedMeta.version !== requestedVersion;
|
|
317
|
-
} else {
|
|
318
|
-
// Latest requested - use cache (user can --update to refresh)
|
|
319
|
-
needsPull = false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (needsPull) {
|
|
324
|
-
// Fetch download info
|
|
325
|
-
const downloadInfo = await client.getDownloadInfo(name, requestedVersion, platform);
|
|
326
|
-
const bundle = downloadInfo.bundle;
|
|
327
|
-
|
|
328
|
-
// Check if cached version is already the latest
|
|
329
|
-
if (cachedMeta && cachedMeta.version === bundle.version && !options.update) {
|
|
330
|
-
needsPull = false;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (needsPull) {
|
|
334
|
-
// Download to temp file
|
|
335
|
-
const tempPath = join(homedir(), '.mpak', 'tmp', `${Date.now()}.mcpb`);
|
|
336
|
-
mkdirSync(dirname(tempPath), { recursive: true });
|
|
337
|
-
|
|
338
|
-
process.stderr.write(`=> Pulling ${name}@${bundle.version}...\n`);
|
|
339
|
-
await client.downloadBundle(downloadInfo.url, tempPath);
|
|
340
|
-
|
|
341
|
-
// Clear old cache and extract
|
|
342
|
-
const { rmSync } = await import('fs');
|
|
343
|
-
if (existsSync(cacheDir)) {
|
|
344
|
-
rmSync(cacheDir, { recursive: true, force: true });
|
|
345
|
-
}
|
|
346
|
-
mkdirSync(cacheDir, { recursive: true });
|
|
347
|
-
|
|
348
|
-
await extractZip(tempPath, cacheDir);
|
|
349
|
-
|
|
350
|
-
// Write metadata
|
|
351
|
-
writeCacheMetadata(cacheDir, {
|
|
352
|
-
version: bundle.version,
|
|
353
|
-
pulledAt: new Date().toISOString(),
|
|
354
|
-
platform: bundle.platform,
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
// Cleanup temp file
|
|
358
|
-
rmSync(tempPath, { force: true });
|
|
359
|
-
|
|
360
|
-
process.stderr.write(`=> Cached ${name}@${bundle.version}\n`);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
// Read manifest and execute
|
|
365
|
-
const manifest = readManifest(cacheDir);
|
|
366
|
-
const { type, entry_point, mcp_config } = manifest.server;
|
|
367
|
-
|
|
368
|
-
// Handle user_config substitution
|
|
369
|
-
let userConfigValues: Record<string, string> = {};
|
|
370
|
-
if (manifest.user_config && Object.keys(manifest.user_config).length > 0) {
|
|
371
|
-
const configManager = new ConfigManager();
|
|
372
|
-
userConfigValues = await gatherUserConfigValues(name, manifest.user_config, configManager);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Substitute user_config placeholders in env vars
|
|
376
|
-
// Priority: process.env (from parent like Claude Desktop) > substituted values (from mpak config)
|
|
377
|
-
const substitutedEnv = substituteEnvVars(mcp_config.env, userConfigValues);
|
|
378
|
-
|
|
379
|
-
let command: string;
|
|
380
|
-
let args: string[];
|
|
381
|
-
let env: Record<string, string | undefined> = { ...substitutedEnv, ...process.env };
|
|
382
|
-
|
|
383
|
-
switch (type) {
|
|
384
|
-
case 'binary': {
|
|
385
|
-
// For binary, the entry_point is the executable path relative to bundle
|
|
386
|
-
command = join(cacheDir, entry_point);
|
|
387
|
-
args = resolveArgs(mcp_config.args || [], cacheDir);
|
|
388
|
-
|
|
389
|
-
// Ensure binary is executable
|
|
390
|
-
try {
|
|
391
|
-
chmodSync(command, 0o755);
|
|
392
|
-
} catch {
|
|
393
|
-
// Ignore chmod errors on Windows
|
|
394
|
-
}
|
|
395
|
-
break;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
case 'node': {
|
|
399
|
-
command = mcp_config.command || 'node';
|
|
400
|
-
// Use mcp_config.args directly if provided, otherwise fall back to entry_point
|
|
401
|
-
if (mcp_config.args && mcp_config.args.length > 0) {
|
|
402
|
-
args = resolveArgs(mcp_config.args, cacheDir);
|
|
403
|
-
} else {
|
|
404
|
-
args = [join(cacheDir, entry_point)];
|
|
405
|
-
}
|
|
406
|
-
break;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
case 'python': {
|
|
410
|
-
// Use manifest command if specified, otherwise auto-detect python
|
|
411
|
-
command = mcp_config.command === 'python' ? findPythonCommand() : (mcp_config.command || findPythonCommand());
|
|
412
|
-
|
|
413
|
-
// Use mcp_config.args directly if provided, otherwise fall back to entry_point
|
|
414
|
-
if (mcp_config.args && mcp_config.args.length > 0) {
|
|
415
|
-
args = resolveArgs(mcp_config.args, cacheDir);
|
|
416
|
-
} else {
|
|
417
|
-
args = [join(cacheDir, entry_point)];
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Set PYTHONPATH to deps/ directory for dependency resolution
|
|
421
|
-
const depsDir = join(cacheDir, 'deps');
|
|
422
|
-
const existingPythonPath = process.env.PYTHONPATH;
|
|
423
|
-
env.PYTHONPATH = existingPythonPath ? `${depsDir}:${existingPythonPath}` : depsDir;
|
|
424
|
-
break;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
default:
|
|
428
|
-
throw new Error(`Unsupported server type: ${type}`);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// Spawn with stdio passthrough for MCP
|
|
432
|
-
const child = spawn(command, args, {
|
|
433
|
-
stdio: ['inherit', 'inherit', 'inherit'],
|
|
434
|
-
env,
|
|
435
|
-
cwd: cacheDir,
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
// Forward signals
|
|
439
|
-
process.on('SIGINT', () => child.kill('SIGINT'));
|
|
440
|
-
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
441
|
-
|
|
442
|
-
// Wait for exit
|
|
443
|
-
child.on('exit', (code) => {
|
|
444
|
-
process.exit(code ?? 0);
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
child.on('error', (error) => {
|
|
448
|
-
process.stderr.write(`=> Failed to start server: ${error.message}\n`);
|
|
449
|
-
process.exit(1);
|
|
450
|
-
});
|
|
451
|
-
}
|