@artyfacts/claude 1.3.8 → 1.3.10

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/src/mcp.ts ADDED
@@ -0,0 +1,314 @@
1
+ /**
2
+ * MCP Connection Handler
3
+ *
4
+ * Handles mcp_connect_request events from Artyfacts and configures
5
+ * MCP servers for the Claude adapter using `claude mcp add`.
6
+ *
7
+ * @module mcp
8
+ */
9
+
10
+ import { spawnSync } from 'child_process';
11
+ import { McpConnectRequestEvent } from './listener';
12
+
13
+ // ============================================================================
14
+ // Types
15
+ // ============================================================================
16
+
17
+ export interface McpServerConfig {
18
+ command: string;
19
+ args?: string[];
20
+ env?: Record<string, string>;
21
+ }
22
+
23
+ export interface ClaudeSettingsJson {
24
+ mcpServers?: Record<string, McpServerConfig>;
25
+ }
26
+
27
+ export interface McpHandlerConfig {
28
+ /** Artyfacts API key for status updates */
29
+ apiKey: string;
30
+ /** Base URL for Artyfacts API */
31
+ baseUrl?: string;
32
+ /** Callback when MCP server is configured */
33
+ onConfigured?: (connectionId: string, platform: string) => void;
34
+ /** Callback on error */
35
+ onError?: (error: Error, connectionId: string) => void;
36
+ }
37
+
38
+ // ============================================================================
39
+ // Constants
40
+ // ============================================================================
41
+
42
+ const DEFAULT_BASE_URL = 'https://artyfacts.dev/api/v1';
43
+
44
+ // ============================================================================
45
+ // MCP Server Configurations
46
+ // ============================================================================
47
+
48
+ /**
49
+ * URL-based MCP servers with built-in OAuth
50
+ * These open a browser for authentication automatically - no credentials needed!
51
+ * Uses `claude mcp add --transport http <name> <url>`
52
+ */
53
+ const OAUTH_MCP_SERVERS: Record<string, { url: string; name: string }> = {
54
+ supabase: {
55
+ url: 'https://mcp.supabase.com/mcp',
56
+ name: 'supabase',
57
+ },
58
+ figma: {
59
+ url: 'https://mcp.figma.com/mcp',
60
+ name: 'figma',
61
+ },
62
+ };
63
+
64
+ /**
65
+ * Command-based MCP servers that need credentials
66
+ * Uses `claude mcp add <name> -- <command> <args...>`
67
+ */
68
+ const CREDENTIAL_MCP_CONFIGS: Record<string, (config: Record<string, unknown>) => {
69
+ command: string;
70
+ args: string[];
71
+ env?: Record<string, string>;
72
+ }> = {
73
+ postgres: (config) => ({
74
+ command: 'npx',
75
+ args: ['-y', '@modelcontextprotocol/server-postgres', config.connection_string as string],
76
+ }),
77
+ github: (config) => ({
78
+ command: 'npx',
79
+ args: ['-y', '@modelcontextprotocol/server-github'],
80
+ env: {
81
+ GITHUB_PERSONAL_ACCESS_TOKEN: config.api_key as string,
82
+ },
83
+ }),
84
+ filesystem: (config) => ({
85
+ command: 'npx',
86
+ args: ['-y', '@modelcontextprotocol/server-filesystem', ...(config.paths as string[] || [])],
87
+ }),
88
+ };
89
+
90
+ // ============================================================================
91
+ // Helper Functions
92
+ // ============================================================================
93
+
94
+ /**
95
+ * Add an MCP server using `claude mcp add`
96
+ */
97
+ function addMcpServer(options: {
98
+ name: string;
99
+ transport: 'http' | 'stdio';
100
+ url?: string;
101
+ command?: string;
102
+ args?: string[];
103
+ env?: Record<string, string>;
104
+ scope?: 'local' | 'user' | 'project';
105
+ }): { success: boolean; error?: string } {
106
+ const { name, transport, url, command, args = [], env = {}, scope = 'user' } = options;
107
+
108
+ const cliArgs = ['mcp', 'add', '-s', scope, '-t', transport];
109
+
110
+ // Add environment variables
111
+ for (const [key, value] of Object.entries(env)) {
112
+ cliArgs.push('-e', `${key}=${value}`);
113
+ }
114
+
115
+ // Add name
116
+ cliArgs.push(name);
117
+
118
+ if (transport === 'http' && url) {
119
+ // HTTP transport: claude mcp add -t http <name> <url>
120
+ cliArgs.push(url);
121
+ } else if (transport === 'stdio' && command) {
122
+ // Stdio transport: claude mcp add <name> -- <command> <args...>
123
+ cliArgs.push('--', command, ...args);
124
+ } else {
125
+ return { success: false, error: 'Invalid configuration: missing url or command' };
126
+ }
127
+
128
+ console.log(`[MCP] Running: claude ${cliArgs.join(' ')}`);
129
+
130
+ const result = spawnSync('claude', cliArgs, {
131
+ encoding: 'utf-8',
132
+ stdio: ['pipe', 'pipe', 'pipe'],
133
+ });
134
+
135
+ if (result.status === 0) {
136
+ return { success: true };
137
+ } else {
138
+ return {
139
+ success: false,
140
+ error: result.stderr || result.stdout || `Exit code ${result.status}`,
141
+ };
142
+ }
143
+ }
144
+
145
+ // ============================================================================
146
+ // McpHandler Class
147
+ // ============================================================================
148
+
149
+ export class McpHandler {
150
+ private config: Required<Pick<McpHandlerConfig, 'apiKey' | 'baseUrl'>> & McpHandlerConfig;
151
+
152
+ constructor(config: McpHandlerConfig) {
153
+ this.config = {
154
+ ...config,
155
+ baseUrl: config.baseUrl || DEFAULT_BASE_URL,
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Handle an MCP connect request event
161
+ * Uses `claude mcp add` to configure the server dynamically
162
+ */
163
+ async handleConnectRequest(event: McpConnectRequestEvent): Promise<void> {
164
+ const { connection_id, platform, config: mcpConfig } = event.data;
165
+
166
+ try {
167
+ const serverName = mcpConfig?.server_name || platform;
168
+
169
+ // Check if this is an OAuth-based server (no credentials needed!)
170
+ const oauthServer = OAUTH_MCP_SERVERS[platform];
171
+ if (oauthServer) {
172
+ // URL-based MCP server with built-in OAuth
173
+ const result = addMcpServer({
174
+ name: serverName,
175
+ transport: 'http',
176
+ url: oauthServer.url,
177
+ });
178
+
179
+ if (!result.success) {
180
+ throw new Error(`Failed to add ${platform} MCP: ${result.error}`);
181
+ }
182
+
183
+ console.log(`[MCP] Added ${serverName} → ${oauthServer.url}`);
184
+ console.log(`[MCP] OAuth will prompt when you use ${platform} tools`);
185
+ } else {
186
+ // Command-based MCP server - needs credentials
187
+ const configBuilder = CREDENTIAL_MCP_CONFIGS[platform];
188
+ if (!configBuilder) {
189
+ throw new Error(`Unsupported MCP platform: ${platform}. Supported: ${[...Object.keys(OAUTH_MCP_SERVERS), ...Object.keys(CREDENTIAL_MCP_CONFIGS)].join(', ')}`);
190
+ }
191
+
192
+ if (!mcpConfig) {
193
+ throw new Error(`Platform ${platform} requires configuration (connection_string or api_key)`);
194
+ }
195
+
196
+ const serverConfig = configBuilder(mcpConfig);
197
+ const result = addMcpServer({
198
+ name: serverName,
199
+ transport: 'stdio',
200
+ command: serverConfig.command,
201
+ args: serverConfig.args,
202
+ env: serverConfig.env,
203
+ });
204
+
205
+ if (!result.success) {
206
+ throw new Error(`Failed to add ${platform} MCP: ${result.error}`);
207
+ }
208
+
209
+ console.log(`[MCP] Added ${serverName} for ${platform}`);
210
+ }
211
+
212
+ // Update connection status in Artyfacts
213
+ await this.updateConnectionStatus(connection_id, {
214
+ status: 'active',
215
+ mcp_configured: true,
216
+ });
217
+
218
+ // Callback
219
+ this.config.onConfigured?.(connection_id, platform);
220
+
221
+ } catch (err) {
222
+ console.error(`[MCP] Failed to configure ${platform}:`, err);
223
+
224
+ // Update connection with error status
225
+ await this.updateConnectionStatus(connection_id, {
226
+ status: 'error',
227
+ mcp_configured: false,
228
+ error_message: (err as Error).message,
229
+ });
230
+
231
+ this.config.onError?.(err as Error, connection_id);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Check if a platform supports OAuth (no credentials needed)
237
+ */
238
+ static supportsOAuth(platform: string): boolean {
239
+ return platform in OAUTH_MCP_SERVERS;
240
+ }
241
+
242
+ /**
243
+ * Get list of platforms with OAuth support
244
+ */
245
+ static getOAuthPlatforms(): string[] {
246
+ return Object.keys(OAUTH_MCP_SERVERS);
247
+ }
248
+
249
+ /**
250
+ * Update connection status in Artyfacts
251
+ */
252
+ private async updateConnectionStatus(
253
+ connectionId: string,
254
+ update: { status: string; mcp_configured: boolean; error_message?: string }
255
+ ): Promise<void> {
256
+ try {
257
+ const response = await fetch(`${this.config.baseUrl}/connections/${connectionId}`, {
258
+ method: 'PATCH',
259
+ headers: {
260
+ 'Authorization': `Bearer ${this.config.apiKey}`,
261
+ 'Content-Type': 'application/json',
262
+ },
263
+ body: JSON.stringify(update),
264
+ });
265
+
266
+ if (!response.ok) {
267
+ console.error(`[MCP] Failed to update connection status: ${response.status}`);
268
+ }
269
+ } catch (err) {
270
+ console.error('[MCP] Failed to update connection status:', err);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * List configured MCP servers using `claude mcp list`
276
+ */
277
+ listServers(): string[] {
278
+ const result = spawnSync('claude', ['mcp', 'list'], {
279
+ encoding: 'utf-8',
280
+ stdio: ['pipe', 'pipe', 'pipe'],
281
+ });
282
+
283
+ if (result.status === 0 && result.stdout) {
284
+ // Parse output - each line is a server name
285
+ return result.stdout.trim().split('\n').filter(Boolean);
286
+ }
287
+ return [];
288
+ }
289
+
290
+ /**
291
+ * Remove an MCP server using `claude mcp remove`
292
+ */
293
+ removeServer(serverName: string): boolean {
294
+ const result = spawnSync('claude', ['mcp', 'remove', serverName], {
295
+ encoding: 'utf-8',
296
+ stdio: ['pipe', 'pipe', 'pipe'],
297
+ });
298
+
299
+ if (result.status === 0) {
300
+ console.log(`[MCP] Removed server: ${serverName}`);
301
+ return true;
302
+ } else {
303
+ console.error(`[MCP] Failed to remove ${serverName}: ${result.stderr || result.stdout}`);
304
+ return false;
305
+ }
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Create an MCP handler instance
311
+ */
312
+ export function createMcpHandler(config: McpHandlerConfig): McpHandler {
313
+ return new McpHandler(config);
314
+ }