@metalabdesign/mcp-client 1.0.1

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.
Files changed (64) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +243 -0
  3. package/dist/bin/cli.d.ts +14 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/bin/cli.js +216 -0
  6. package/dist/bin/cli.js.map +1 -0
  7. package/dist/bin/metalab-mcp-config.d.ts +9 -0
  8. package/dist/bin/metalab-mcp-config.d.ts.map +1 -0
  9. package/dist/bin/metalab-mcp-config.js +224 -0
  10. package/dist/bin/metalab-mcp-config.js.map +1 -0
  11. package/dist/bin/metalab-mcp.d.ts +14 -0
  12. package/dist/bin/metalab-mcp.d.ts.map +1 -0
  13. package/dist/bin/metalab-mcp.js +242 -0
  14. package/dist/bin/metalab-mcp.js.map +1 -0
  15. package/dist/config/aws-sso.d.ts +21 -0
  16. package/dist/config/aws-sso.d.ts.map +1 -0
  17. package/dist/config/aws-sso.js +67 -0
  18. package/dist/config/aws-sso.js.map +1 -0
  19. package/dist/config/defaults.d.ts +14 -0
  20. package/dist/config/defaults.d.ts.map +1 -0
  21. package/dist/config/defaults.js +20 -0
  22. package/dist/config/defaults.js.map +1 -0
  23. package/dist/config/index.d.ts +5 -0
  24. package/dist/config/index.d.ts.map +1 -0
  25. package/dist/config/index.js +3 -0
  26. package/dist/config/index.js.map +1 -0
  27. package/dist/index.d.ts +27 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +30 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/oauth.d.ts +34 -0
  32. package/dist/oauth.d.ts.map +1 -0
  33. package/dist/oauth.js +401 -0
  34. package/dist/oauth.js.map +1 -0
  35. package/dist/proxy.d.ts +39 -0
  36. package/dist/proxy.d.ts.map +1 -0
  37. package/dist/proxy.js +203 -0
  38. package/dist/proxy.js.map +1 -0
  39. package/dist/storage.d.ts +15 -0
  40. package/dist/storage.d.ts.map +1 -0
  41. package/dist/storage.js +64 -0
  42. package/dist/storage.js.map +1 -0
  43. package/dist/types.d.ts +78 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +40 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/utils.d.ts +42 -0
  48. package/dist/utils.d.ts.map +1 -0
  49. package/dist/utils.js +89 -0
  50. package/dist/utils.js.map +1 -0
  51. package/package.json +51 -0
  52. package/src/bin/cli.ts +242 -0
  53. package/src/bin/metalab-mcp-config.ts +262 -0
  54. package/src/bin/metalab-mcp.ts +284 -0
  55. package/src/config/aws-sso.ts +78 -0
  56. package/src/config/defaults.ts +26 -0
  57. package/src/config/index.ts +8 -0
  58. package/src/index.ts +54 -0
  59. package/src/oauth.ts +540 -0
  60. package/src/proxy.ts +274 -0
  61. package/src/storage.ts +81 -0
  62. package/src/types.ts +79 -0
  63. package/src/utils.ts +115 -0
  64. package/tsconfig.json +25 -0
@@ -0,0 +1,284 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Metalab MCP Client - Zero-config connection to Metalab's MCP Gateway
4
+ *
5
+ * Usage:
6
+ * metalab-mcp [options]
7
+ *
8
+ * The gateway URL is auto-detected from:
9
+ * 1. AWS SSM Parameter Store (requires AWS credentials)
10
+ * 2. MCP_SERVER_URL environment variable
11
+ * 3. Hardcoded production URL (fallback)
12
+ */
13
+
14
+ import { spawn } from 'node:child_process';
15
+ import { createProxy } from '../index.js';
16
+ import { resolveGatewayUrl, getSourceDescription, DEFAULTS } from '../config/index.js';
17
+
18
+ interface CliArgs {
19
+ profile?: string;
20
+ callbackUrl?: string;
21
+ tokenStorageDir?: string;
22
+ timeout?: number;
23
+ openBrowser?: boolean;
24
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
25
+ serviceTokens?: Record<string, string>;
26
+ inspect?: boolean;
27
+ help?: boolean;
28
+ }
29
+
30
+ function printBanner(config: {
31
+ gatewayUrl: string;
32
+ source: string;
33
+ profile?: string;
34
+ callbackUrl: string;
35
+ figmaToken?: string;
36
+ notionToken?: string;
37
+ }): void {
38
+ const truncateUrl = (url: string, maxLen: number = 50): string => {
39
+ if (url.length <= maxLen) return url;
40
+ return url.substring(0, maxLen - 3) + '...';
41
+ };
42
+
43
+ const checkmark = '\u2713';
44
+ const cross = '\u2717';
45
+
46
+ console.error('');
47
+ console.error('\u2554' + '\u2550'.repeat(62) + '\u2557');
48
+ console.error('\u2551' + ' Metalab MCP Client '.substring(0, 62) + '\u2551');
49
+ console.error('\u2560' + '\u2550'.repeat(62) + '\u2563');
50
+ console.error(`\u2551 Gateway URL: ${truncateUrl(config.gatewayUrl, 45).padEnd(46)}\u2551`);
51
+ console.error(`\u2551 Source: ${config.source.padEnd(46)}\u2551`);
52
+ if (config.profile) {
53
+ console.error(`\u2551 AWS Profile: ${config.profile.padEnd(46)}\u2551`);
54
+ }
55
+ console.error(`\u2551 Callback: ${truncateUrl(config.callbackUrl, 45).padEnd(46)}\u2551`);
56
+ console.error(`\u2551 Figma Token: ${(config.figmaToken ? `${checkmark} configured` : `${cross} not configured`).padEnd(46)}\u2551`);
57
+ console.error(`\u2551 Notion Token: ${(config.notionToken ? `${checkmark} configured` : `${cross} not configured`).padEnd(45)}\u2551`);
58
+ console.error('\u255A' + '\u2550'.repeat(62) + '\u255D');
59
+ console.error('');
60
+ console.error('Press Ctrl+C to stop');
61
+ console.error('');
62
+ }
63
+
64
+ function printHelp(): void {
65
+ console.error(`
66
+ Metalab MCP Client - Zero-config connection to Metalab's MCP Gateway
67
+
68
+ USAGE:
69
+ metalab-mcp [options]
70
+
71
+ The gateway URL is automatically detected from:
72
+ 1. AWS SSM Parameter Store (if AWS credentials available)
73
+ 2. MCP_SERVER_URL environment variable
74
+ 3. Hardcoded production URL (fallback)
75
+
76
+ OPTIONS:
77
+ --profile <name> AWS SSO profile to use for SSM lookup
78
+ --callback-url <url> OAuth callback URL (default: ${DEFAULTS.CALLBACK_URL})
79
+ --token-dir <dir> Directory for token storage (default: ${DEFAULTS.TOKEN_DIR})
80
+ --timeout <ms> Request timeout in milliseconds (default: ${DEFAULTS.TIMEOUT_MS})
81
+ --no-browser Don't auto-open browser for auth
82
+ --log-level <level> Log level: debug, info, warn, error (default: info)
83
+ --figma-token <token> Figma access token (or set FIGMA_ACCESS_TOKEN env var)
84
+ --notion-token <token> Notion access token (or set NOTION_ACCESS_TOKEN env var)
85
+ --inspect Run with MCP Inspector for debugging
86
+ --help, -h Show this help message
87
+
88
+ EXAMPLES:
89
+ # Basic usage (auto-detects everything)
90
+ metalab-mcp
91
+
92
+ # With AWS SSO profile
93
+ metalab-mcp --profile metalab-dev
94
+
95
+ # With Figma token
96
+ metalab-mcp --figma-token figd_xxxxx
97
+
98
+ # Debug mode with inspector
99
+ metalab-mcp --inspect
100
+
101
+ # Via environment variables
102
+ FIGMA_ACCESS_TOKEN=figd_xxxxx metalab-mcp
103
+
104
+ QUICK SETUP:
105
+ 1. (Optional) Login to AWS SSO: aws sso login --profile metalab-dev
106
+ 2. Generate config: npx @metalabdesign/mcp-client-config --client claude
107
+ 3. Restart your MCP client (Claude Desktop, etc.)
108
+
109
+ For more information, see:
110
+ https://github.com/metalabdesign/metalab-core-mcps
111
+ `);
112
+ }
113
+
114
+ function parseArgs(argv: string[]): CliArgs {
115
+ const args: CliArgs = {
116
+ serviceTokens: {},
117
+ };
118
+ let i = 2; // Skip 'node' and script path
119
+
120
+ while (i < argv.length) {
121
+ const arg = argv[i];
122
+
123
+ switch (arg) {
124
+ case '--profile':
125
+ args.profile = argv[++i];
126
+ break;
127
+ case '--callback-url':
128
+ args.callbackUrl = argv[++i];
129
+ break;
130
+ case '--token-dir':
131
+ args.tokenStorageDir = argv[++i];
132
+ break;
133
+ case '--timeout':
134
+ args.timeout = Number.parseInt(argv[++i] ?? '', 10);
135
+ break;
136
+ case '--no-browser':
137
+ args.openBrowser = false;
138
+ break;
139
+ case '--log-level':
140
+ args.logLevel = argv[++i] as CliArgs['logLevel'];
141
+ break;
142
+ case '--figma-token':
143
+ case '--figma':
144
+ args.serviceTokens!.figma = argv[++i] ?? '';
145
+ break;
146
+ case '--notion-token':
147
+ case '--notion':
148
+ args.serviceTokens!.notion = argv[++i] ?? '';
149
+ break;
150
+ case '--inspect':
151
+ args.inspect = true;
152
+ break;
153
+ case '--help':
154
+ case '-h':
155
+ args.help = true;
156
+ break;
157
+ default:
158
+ if (arg?.startsWith('-')) {
159
+ console.error(`Unknown option: ${arg}`);
160
+ console.error('Run with --help for usage information');
161
+ process.exit(1);
162
+ }
163
+ }
164
+ i++;
165
+ }
166
+
167
+ // Load service tokens from environment variables (fallback)
168
+ if (process.env.FIGMA_ACCESS_TOKEN) {
169
+ args.serviceTokens!.figma = args.serviceTokens!.figma || process.env.FIGMA_ACCESS_TOKEN;
170
+ }
171
+ if (process.env.NOTION_ACCESS_TOKEN) {
172
+ args.serviceTokens!.notion = args.serviceTokens!.notion || process.env.NOTION_ACCESS_TOKEN;
173
+ }
174
+
175
+ // Clean up empty serviceTokens object
176
+ if (Object.keys(args.serviceTokens!).length === 0) {
177
+ delete args.serviceTokens;
178
+ }
179
+
180
+ return args;
181
+ }
182
+
183
+ async function runWithInspector(args: CliArgs, gatewayUrl: string): Promise<void> {
184
+ // Build args for the inspector
185
+ const inspectorArgs = [
186
+ '@modelcontextprotocol/inspector',
187
+ 'node',
188
+ new URL('../bin/cli.js', import.meta.url).pathname,
189
+ '--remote-url',
190
+ gatewayUrl + '/mcp',
191
+ '--callback-url',
192
+ args.callbackUrl || DEFAULTS.CALLBACK_URL,
193
+ ];
194
+
195
+ if (args.tokenStorageDir) {
196
+ inspectorArgs.push('--token-dir', args.tokenStorageDir);
197
+ }
198
+ if (args.timeout) {
199
+ inspectorArgs.push('--timeout', args.timeout.toString());
200
+ }
201
+ if (args.openBrowser === false) {
202
+ inspectorArgs.push('--no-browser');
203
+ }
204
+ if (args.logLevel) {
205
+ inspectorArgs.push('--log-level', args.logLevel);
206
+ }
207
+ if (args.serviceTokens?.figma) {
208
+ inspectorArgs.push('--figma-token', args.serviceTokens.figma);
209
+ }
210
+ if (args.serviceTokens?.notion) {
211
+ inspectorArgs.push('--notion-token', args.serviceTokens.notion);
212
+ }
213
+
214
+ console.error('Starting MCP Inspector...');
215
+ console.error('');
216
+
217
+ const child = spawn('npx', inspectorArgs, {
218
+ stdio: 'inherit',
219
+ shell: true,
220
+ });
221
+
222
+ child.on('exit', (code) => {
223
+ process.exit(code ?? 0);
224
+ });
225
+ }
226
+
227
+ async function main(): Promise<void> {
228
+ const args = parseArgs(process.argv);
229
+
230
+ if (args.help) {
231
+ printHelp();
232
+ process.exit(0);
233
+ }
234
+
235
+ // Resolve gateway URL
236
+ const config = await resolveGatewayUrl(args.profile);
237
+ const callbackUrl = args.callbackUrl || DEFAULTS.CALLBACK_URL;
238
+
239
+ // Print banner
240
+ printBanner({
241
+ gatewayUrl: config.gatewayUrl,
242
+ source: getSourceDescription(config.source),
243
+ profile: config.profile,
244
+ callbackUrl,
245
+ figmaToken: args.serviceTokens?.figma,
246
+ notionToken: args.serviceTokens?.notion,
247
+ });
248
+
249
+ // If --inspect flag, run with MCP Inspector
250
+ if (args.inspect) {
251
+ await runWithInspector(args, config.gatewayUrl);
252
+ return;
253
+ }
254
+
255
+ try {
256
+ const proxy = await createProxy({
257
+ remoteUrl: config.gatewayUrl + '/mcp',
258
+ callbackUrl,
259
+ tokenStorageDir: args.tokenStorageDir,
260
+ timeout: args.timeout,
261
+ openBrowser: args.openBrowser ?? true,
262
+ logLevel: args.logLevel ?? 'info',
263
+ serviceTokens: args.serviceTokens,
264
+ });
265
+
266
+ // Handle shutdown gracefully
267
+ const shutdown = async (): Promise<void> => {
268
+ console.error('Shutting down...');
269
+ await proxy.disconnect();
270
+ process.exit(0);
271
+ };
272
+
273
+ process.on('SIGINT', shutdown);
274
+ process.on('SIGTERM', shutdown);
275
+
276
+ // Start the proxy
277
+ await proxy.start();
278
+ } catch (error) {
279
+ console.error('Failed to start proxy:', error);
280
+ process.exit(1);
281
+ }
282
+ }
283
+
284
+ main();
@@ -0,0 +1,78 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { DEFAULTS } from './defaults.js';
3
+
4
+ export interface AwsConfig {
5
+ gatewayUrl: string;
6
+ source: 'ssm' | 'env' | 'hardcoded';
7
+ profile?: string;
8
+ }
9
+
10
+ /**
11
+ * Resolution order:
12
+ * 1. AWS SSO/credentials → SSM Parameter Store → gateway URL
13
+ * 2. MCP_SERVER_URL environment variable
14
+ * 3. Hardcoded production URL (fallback)
15
+ */
16
+ export async function resolveGatewayUrl(profile?: string): Promise<AwsConfig> {
17
+ // Try SSM Parameter Store first (requires AWS credentials)
18
+ const ssmUrl = await getUrlFromSsm(profile);
19
+ if (ssmUrl) {
20
+ return { gatewayUrl: ssmUrl, source: 'ssm', profile };
21
+ }
22
+
23
+ // Fallback to environment variable
24
+ const envUrl = process.env.MCP_SERVER_URL;
25
+ if (envUrl) {
26
+ return { gatewayUrl: envUrl, source: 'env' };
27
+ }
28
+
29
+ // Final fallback to hardcoded URL
30
+ return { gatewayUrl: DEFAULTS.GATEWAY_URL, source: 'hardcoded' };
31
+ }
32
+
33
+ async function getUrlFromSsm(profile?: string): Promise<string | null> {
34
+ try {
35
+ // Check if AWS CLI is available
36
+ execSync('which aws', { encoding: 'utf-8', stdio: 'pipe' });
37
+
38
+ // Build the AWS CLI command
39
+ const profileArg = profile ? `--profile ${profile}` : '';
40
+ const cmd = `aws ssm get-parameter ${profileArg} --name ${DEFAULTS.SSM_GATEWAY_URL_PARAM} --region ${DEFAULTS.AWS_REGION} --query 'Parameter.Value' --output text`;
41
+
42
+ const result = execSync(cmd, { encoding: 'utf-8', stdio: 'pipe' }).trim();
43
+ return result || null;
44
+ } catch {
45
+ // AWS CLI not available, not authenticated, or parameter doesn't exist
46
+ return null;
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Check if AWS SSO session is active for the given profile
52
+ */
53
+ export function isSsoAuthenticated(profile?: string): boolean {
54
+ try {
55
+ const profileArg = profile ? `--profile ${profile}` : '';
56
+ execSync(`aws sts get-caller-identity ${profileArg}`, {
57
+ encoding: 'utf-8',
58
+ stdio: 'pipe',
59
+ });
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get a human-readable description of the gateway URL source
68
+ */
69
+ export function getSourceDescription(source: AwsConfig['source']): string {
70
+ switch (source) {
71
+ case 'ssm':
72
+ return 'SSM Parameter Store';
73
+ case 'env':
74
+ return 'MCP_SERVER_URL environment variable';
75
+ case 'hardcoded':
76
+ return 'Default (hardcoded)';
77
+ }
78
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Default configuration values for @metalabdesign/mcp-client
3
+ */
4
+ export const DEFAULTS = {
5
+ // Production MCP Gateway URL (hardcoded fallback)
6
+ // This is the current API Gateway URL - will be updated if infrastructure changes
7
+ GATEWAY_URL: 'https://qe40bdp1jh.execute-api.us-east-2.amazonaws.com',
8
+
9
+ // SSM Parameter path for dynamic gateway URL
10
+ SSM_GATEWAY_URL_PARAM: '/mcp/gateway-url',
11
+
12
+ // AWS Region for SSM lookups
13
+ AWS_REGION: 'us-east-2',
14
+
15
+ // OAuth callback URL (matches Okta redirect URI configuration)
16
+ CALLBACK_URL: 'http://localhost:9861/oauth/callback',
17
+
18
+ // Token storage directory
19
+ TOKEN_DIR: '~/.metalab/mcp-client/tokens',
20
+
21
+ // Timeouts
22
+ TIMEOUT_MS: 30000,
23
+ AUTH_TIMEOUT_MS: 300000,
24
+ } as const;
25
+
26
+ export type Defaults = typeof DEFAULTS;
@@ -0,0 +1,8 @@
1
+ export { DEFAULTS } from './defaults.js';
2
+ export type { Defaults } from './defaults.js';
3
+ export {
4
+ resolveGatewayUrl,
5
+ isSsoAuthenticated,
6
+ getSourceDescription,
7
+ } from './aws-sso.js';
8
+ export type { AwsConfig } from './aws-sso.js';
package/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * MCP Local Proxy - Factory and exports
3
+ */
4
+
5
+ import type { McpProxy } from './proxy.js';
6
+
7
+ export interface CreateProxyOptions {
8
+ /** Remote MCP server URL */
9
+ remoteUrl: string;
10
+ /** OAuth callback URL (port is extracted from this URL) */
11
+ callbackUrl: string;
12
+ /** Directory for token storage */
13
+ tokenStorageDir?: string;
14
+ /** Custom headers for remote requests */
15
+ headers?: Record<string, string>;
16
+ /** Request timeout in ms (default: 30000) */
17
+ timeout?: number;
18
+ /** Auto-open browser for auth (default: true) */
19
+ openBrowser?: boolean;
20
+ /** Log level (default: 'info') */
21
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
22
+ /** Service-specific access tokens (e.g., { figma: "token", notion: "token" }) */
23
+ serviceTokens?: Record<string, string>;
24
+ }
25
+
26
+ /**
27
+ * Create and configure an MCP OAuth proxy
28
+ */
29
+ export async function createProxy(options: CreateProxyOptions): Promise<McpProxy> {
30
+ const { ProxyConfigSchema } = await import('./types.js');
31
+ const { TokenStorage } = await import('./storage.js');
32
+ const { OAuthManager } = await import('./oauth.js');
33
+ const { McpProxy } = await import('./proxy.js');
34
+ const { StderrLogger } = await import('./utils.js');
35
+
36
+ // Validate and parse configuration
37
+ const config = ProxyConfigSchema.parse({
38
+ remoteUrl: options.remoteUrl,
39
+ callbackUrl: options.callbackUrl,
40
+ tokenStorageDir: options.tokenStorageDir,
41
+ headers: options.headers,
42
+ timeout: options.timeout ?? 30000,
43
+ openBrowser: options.openBrowser ?? true,
44
+ serviceTokens: options.serviceTokens,
45
+ });
46
+
47
+ // Create components
48
+ const logger = new StderrLogger(options.logLevel ?? 'info');
49
+ const storage = new TokenStorage(config.tokenStorageDir);
50
+ const oauthManager = new OAuthManager(config, storage, logger);
51
+ const proxy = new McpProxy(config, oauthManager, logger);
52
+
53
+ return proxy;
54
+ }