@ruifung/codemode-bridge 1.0.3-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.
- package/LICENSE +202 -0
- package/README.md +378 -0
- package/dist/cli/commands.d.ts +70 -0
- package/dist/cli/commands.js +436 -0
- package/dist/cli/config-manager.d.ts +53 -0
- package/dist/cli/config-manager.js +142 -0
- package/dist/cli/index.d.ts +19 -0
- package/dist/cli/index.js +165 -0
- package/dist/executor/container-executor.d.ts +81 -0
- package/dist/executor/container-executor.js +351 -0
- package/dist/executor/executor-test-suite.d.ts +22 -0
- package/dist/executor/executor-test-suite.js +395 -0
- package/dist/executor/isolated-vm-executor.d.ts +78 -0
- package/dist/executor/isolated-vm-executor.js +368 -0
- package/dist/executor/vm2-executor.d.ts +21 -0
- package/dist/executor/vm2-executor.js +109 -0
- package/dist/executor/wrap-code.d.ts +52 -0
- package/dist/executor/wrap-code.js +80 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/mcp/config.d.ts +44 -0
- package/dist/mcp/config.js +102 -0
- package/dist/mcp/e2e-bridge-test-suite.d.ts +28 -0
- package/dist/mcp/e2e-bridge-test-suite.js +429 -0
- package/dist/mcp/executor.d.ts +31 -0
- package/dist/mcp/executor.js +121 -0
- package/dist/mcp/mcp-adapter.d.ts +12 -0
- package/dist/mcp/mcp-adapter.js +49 -0
- package/dist/mcp/mcp-client.d.ts +85 -0
- package/dist/mcp/mcp-client.js +441 -0
- package/dist/mcp/oauth-handler.d.ts +33 -0
- package/dist/mcp/oauth-handler.js +138 -0
- package/dist/mcp/server.d.ts +25 -0
- package/dist/mcp/server.js +322 -0
- package/dist/mcp/token-persistence.d.ts +57 -0
- package/dist/mcp/token-persistence.js +131 -0
- package/dist/utils/logger.d.ts +44 -0
- package/dist/utils/logger.js +123 -0
- package/package.json +56 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Server - Exposes the Code Mode bridge as an MCP server
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* - Upstream: Use official MCP SDK's Client to connect to and collect tools from other MCP servers
|
|
6
|
+
* - Orchestration: Pass collected tools to codemode SDK's createCodeTool()
|
|
7
|
+
* - Downstream: Use MCP SDK to expose the codemode tool via MCP protocol (stdio transport)
|
|
8
|
+
*
|
|
9
|
+
* This server:
|
|
10
|
+
* 1. Connects to upstream MCP servers using official MCP SDK Client
|
|
11
|
+
* 2. Collects tools from all upstream servers in native MCP format (JSON Schema)
|
|
12
|
+
* 3. Converts tools to ToolDescriptor format (with Zod schemas)
|
|
13
|
+
* 4. Uses @cloudflare/codemode SDK to create the "codemode" tool with those tools
|
|
14
|
+
* 5. Adapts the codemode SDK's AI SDK Tool to MCP protocol using a shim layer
|
|
15
|
+
* 6. Exposes the "codemode" tool via MCP protocol downstream
|
|
16
|
+
*/
|
|
17
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
+
import { createCodeTool } from "@cloudflare/codemode/ai";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { createExecutor } from "./executor.js";
|
|
22
|
+
import { adaptAISDKToolToMCP } from "./mcp-adapter.js";
|
|
23
|
+
import { MCPClient } from "./mcp-client.js";
|
|
24
|
+
import { logDebug, logError, logInfo, enableStderrBuffering } from "../utils/logger.js";
|
|
25
|
+
/**
|
|
26
|
+
* Convert JSON Schema to Zod schema
|
|
27
|
+
* MCP tools use JSON Schema, but createCodeTool expects Zod schemas
|
|
28
|
+
*/
|
|
29
|
+
export function jsonSchemaToZod(schema) {
|
|
30
|
+
// Handle null/undefined
|
|
31
|
+
if (!schema) {
|
|
32
|
+
return z.object({}).strict();
|
|
33
|
+
}
|
|
34
|
+
// Handle object type
|
|
35
|
+
if (schema.type === "object" || !schema.type) {
|
|
36
|
+
const props = {};
|
|
37
|
+
if (schema.properties) {
|
|
38
|
+
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
39
|
+
props[key] = jsonSchemaToZod(prop);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (schema.required && Array.isArray(schema.required)) {
|
|
43
|
+
const required = new Set(schema.required);
|
|
44
|
+
const finalProps = {};
|
|
45
|
+
for (const [key, zodSchema] of Object.entries(props)) {
|
|
46
|
+
if (required.has(key)) {
|
|
47
|
+
finalProps[key] = zodSchema;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
finalProps[key] = zodSchema.optional();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return z.object(finalProps).strict();
|
|
54
|
+
}
|
|
55
|
+
// Make all fields optional if no required list
|
|
56
|
+
const optionalProps = {};
|
|
57
|
+
for (const [key, zodSchema] of Object.entries(props)) {
|
|
58
|
+
optionalProps[key] = zodSchema.optional();
|
|
59
|
+
}
|
|
60
|
+
return z.object(optionalProps).strict();
|
|
61
|
+
}
|
|
62
|
+
// Handle array type
|
|
63
|
+
if (schema.type === "array") {
|
|
64
|
+
const itemSchema = schema.items ? jsonSchemaToZod(schema.items) : z.any();
|
|
65
|
+
let arraySchema = z.array(itemSchema);
|
|
66
|
+
// Apply array constraints
|
|
67
|
+
if (typeof schema.minItems === "number") {
|
|
68
|
+
arraySchema = arraySchema.min(schema.minItems);
|
|
69
|
+
}
|
|
70
|
+
if (typeof schema.maxItems === "number") {
|
|
71
|
+
arraySchema = arraySchema.max(schema.maxItems);
|
|
72
|
+
}
|
|
73
|
+
return arraySchema;
|
|
74
|
+
}
|
|
75
|
+
// Handle string type
|
|
76
|
+
if (schema.type === "string") {
|
|
77
|
+
let stringSchema = z.string();
|
|
78
|
+
// Handle enum
|
|
79
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
80
|
+
return z.enum(schema.enum);
|
|
81
|
+
}
|
|
82
|
+
// Apply string format constraints
|
|
83
|
+
if (schema.format) {
|
|
84
|
+
switch (schema.format) {
|
|
85
|
+
case "email":
|
|
86
|
+
stringSchema = stringSchema.email();
|
|
87
|
+
break;
|
|
88
|
+
case "uuid":
|
|
89
|
+
stringSchema = stringSchema.uuid();
|
|
90
|
+
break;
|
|
91
|
+
case "url":
|
|
92
|
+
stringSchema = stringSchema.url();
|
|
93
|
+
break;
|
|
94
|
+
case "date-time":
|
|
95
|
+
stringSchema = stringSchema.datetime();
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Apply string length constraints
|
|
100
|
+
if (typeof schema.minLength === "number") {
|
|
101
|
+
stringSchema = stringSchema.min(schema.minLength);
|
|
102
|
+
}
|
|
103
|
+
if (typeof schema.maxLength === "number") {
|
|
104
|
+
stringSchema = stringSchema.max(schema.maxLength);
|
|
105
|
+
}
|
|
106
|
+
if (schema.pattern) {
|
|
107
|
+
stringSchema = stringSchema.regex(new RegExp(schema.pattern));
|
|
108
|
+
}
|
|
109
|
+
return stringSchema;
|
|
110
|
+
}
|
|
111
|
+
// Handle number type
|
|
112
|
+
if (schema.type === "number") {
|
|
113
|
+
let numberSchema = z.number();
|
|
114
|
+
// Apply number constraints
|
|
115
|
+
if (typeof schema.minimum === "number") {
|
|
116
|
+
numberSchema = numberSchema.min(schema.minimum);
|
|
117
|
+
}
|
|
118
|
+
if (typeof schema.maximum === "number") {
|
|
119
|
+
numberSchema = numberSchema.max(schema.maximum);
|
|
120
|
+
}
|
|
121
|
+
if (typeof schema.multipleOf === "number") {
|
|
122
|
+
numberSchema = numberSchema.multipleOf(schema.multipleOf);
|
|
123
|
+
}
|
|
124
|
+
return numberSchema;
|
|
125
|
+
}
|
|
126
|
+
// Handle integer type
|
|
127
|
+
if (schema.type === "integer") {
|
|
128
|
+
let intSchema = z.number().int();
|
|
129
|
+
// Apply number constraints
|
|
130
|
+
if (typeof schema.minimum === "number") {
|
|
131
|
+
intSchema = intSchema.min(schema.minimum);
|
|
132
|
+
}
|
|
133
|
+
if (typeof schema.maximum === "number") {
|
|
134
|
+
intSchema = intSchema.max(schema.maximum);
|
|
135
|
+
}
|
|
136
|
+
if (typeof schema.multipleOf === "number") {
|
|
137
|
+
intSchema = intSchema.multipleOf(schema.multipleOf);
|
|
138
|
+
}
|
|
139
|
+
return intSchema;
|
|
140
|
+
}
|
|
141
|
+
// Handle boolean type
|
|
142
|
+
if (schema.type === "boolean") {
|
|
143
|
+
return z.boolean();
|
|
144
|
+
}
|
|
145
|
+
// Handle null type
|
|
146
|
+
if (schema.type === "null") {
|
|
147
|
+
return z.null();
|
|
148
|
+
}
|
|
149
|
+
// Handle anyOf (union types)
|
|
150
|
+
if (schema.anyOf && Array.isArray(schema.anyOf)) {
|
|
151
|
+
const schemas = schema.anyOf.map((s) => jsonSchemaToZod(s));
|
|
152
|
+
return z.union(schemas);
|
|
153
|
+
}
|
|
154
|
+
// Handle oneOf (discriminated union)
|
|
155
|
+
if (schema.oneOf && Array.isArray(schema.oneOf)) {
|
|
156
|
+
const schemas = schema.oneOf.map((s) => jsonSchemaToZod(s));
|
|
157
|
+
return z.union(schemas);
|
|
158
|
+
}
|
|
159
|
+
// Handle allOf (intersection types)
|
|
160
|
+
if (schema.allOf && Array.isArray(schema.allOf)) {
|
|
161
|
+
// Zod doesn't have native intersection for objects, so merge them
|
|
162
|
+
let merged = z.object({});
|
|
163
|
+
for (const subSchema of schema.allOf) {
|
|
164
|
+
const zodSchema = jsonSchemaToZod(subSchema);
|
|
165
|
+
merged = merged.and(zodSchema);
|
|
166
|
+
}
|
|
167
|
+
return merged;
|
|
168
|
+
}
|
|
169
|
+
// Handle enum for non-string types
|
|
170
|
+
if (schema.enum && Array.isArray(schema.enum)) {
|
|
171
|
+
if (schema.enum.length === 1) {
|
|
172
|
+
return z.literal(schema.enum[0]);
|
|
173
|
+
}
|
|
174
|
+
// Create union of literals
|
|
175
|
+
const literals = schema.enum.map((val) => z.literal(val));
|
|
176
|
+
return z.union(literals);
|
|
177
|
+
}
|
|
178
|
+
// Default to any
|
|
179
|
+
return z.any();
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Convert native MCP tool definitions to ToolDescriptor format
|
|
183
|
+
* that createCodeTool() expects
|
|
184
|
+
*/
|
|
185
|
+
function convertMCPToolToDescriptor(toolDef, client, toolName, serverName) {
|
|
186
|
+
return {
|
|
187
|
+
description: toolDef.description || "",
|
|
188
|
+
inputSchema: jsonSchemaToZod(toolDef.inputSchema),
|
|
189
|
+
execute: async (args) => {
|
|
190
|
+
// Log the tool invocation
|
|
191
|
+
logDebug(`Calling tool: ${serverName}__${toolName}`, {
|
|
192
|
+
component: 'Tool Execution',
|
|
193
|
+
server: serverName,
|
|
194
|
+
tool: toolName,
|
|
195
|
+
args: JSON.stringify(args)
|
|
196
|
+
});
|
|
197
|
+
try {
|
|
198
|
+
// Execute the tool on the upstream server using the MCP client
|
|
199
|
+
const result = await client.callTool(toolName, args);
|
|
200
|
+
// Log successful execution
|
|
201
|
+
logDebug(`Tool completed: ${serverName}__${toolName}`, {
|
|
202
|
+
component: 'Tool Execution',
|
|
203
|
+
server: serverName,
|
|
204
|
+
tool: toolName,
|
|
205
|
+
resultType: typeof result,
|
|
206
|
+
resultSize: JSON.stringify(result).length
|
|
207
|
+
});
|
|
208
|
+
return result;
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
logDebug(`Tool failed: ${serverName}__${toolName}`, {
|
|
212
|
+
component: 'Tool Execution',
|
|
213
|
+
server: serverName,
|
|
214
|
+
tool: toolName,
|
|
215
|
+
error: error instanceof Error ? error.message : String(error)
|
|
216
|
+
});
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export async function startCodeModeBridgeServer(serverConfigs) {
|
|
223
|
+
// Enable buffering of stderr output from stdio tools during startup
|
|
224
|
+
enableStderrBuffering();
|
|
225
|
+
const mcp = new McpServer({
|
|
226
|
+
name: "codemode-bridge",
|
|
227
|
+
version: "1.0.0",
|
|
228
|
+
});
|
|
229
|
+
// Collect all tools from upstream MCP servers using official MCP SDK
|
|
230
|
+
const allToolDescriptors = {};
|
|
231
|
+
const toolsByServer = {}; // Track tools grouped by server
|
|
232
|
+
const mcpClients = []; // Keep track of clients for cleanup
|
|
233
|
+
let totalToolCount = 0;
|
|
234
|
+
// Initialize all connections in parallel
|
|
235
|
+
const connectionPromises = serverConfigs.map(async (config) => {
|
|
236
|
+
try {
|
|
237
|
+
// Create client for this upstream MCP server using official SDK
|
|
238
|
+
const client = new MCPClient(config);
|
|
239
|
+
// Connect to the upstream server
|
|
240
|
+
await client.connect();
|
|
241
|
+
mcpClients.push(client);
|
|
242
|
+
// Get tools from this server in native MCP format (JSON Schema)
|
|
243
|
+
const serverTools = await client.listTools();
|
|
244
|
+
const toolCount = serverTools.length;
|
|
245
|
+
totalToolCount += toolCount;
|
|
246
|
+
logDebug(`Server "${config.name}" has ${toolCount} tools`, { component: 'Bridge' });
|
|
247
|
+
// Track tools for this server
|
|
248
|
+
toolsByServer[config.name] = [];
|
|
249
|
+
// Namespace tools by server name to avoid conflicts
|
|
250
|
+
// e.g., kubernetes.get_pod -> kubernetes__get_pod
|
|
251
|
+
// Convert native MCP tools (JSON Schema) to ToolDescriptor format (Zod)
|
|
252
|
+
for (const tool of serverTools) {
|
|
253
|
+
const namespacedName = `${config.name}__${tool.name}`;
|
|
254
|
+
toolsByServer[config.name].push(namespacedName);
|
|
255
|
+
// Convert the native MCP tool to ToolDescriptor format
|
|
256
|
+
const descriptor = convertMCPToolToDescriptor(tool, client, tool.name, config.name);
|
|
257
|
+
allToolDescriptors[namespacedName] = descriptor;
|
|
258
|
+
}
|
|
259
|
+
return { config: config.name, toolCount, success: true };
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
logError(`Failed to connect to "${config.name}"`, error instanceof Error ? error : { error: String(error) });
|
|
263
|
+
// Continue with other servers instead of failing completely
|
|
264
|
+
return { config: config.name, toolCount: 0, success: false };
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// Wait for all connections to initialize in parallel
|
|
268
|
+
const results = await Promise.all(connectionPromises);
|
|
269
|
+
// Recalculate total tool count from results (in case totalToolCount wasn't updated due to timing)
|
|
270
|
+
totalToolCount = results.reduce((sum, result) => sum + (result?.toolCount || 0), 0);
|
|
271
|
+
logInfo(`Total: ${totalToolCount} tools from ${serverConfigs.length} server(s)`, { component: 'Bridge' });
|
|
272
|
+
// Log tools grouped by server
|
|
273
|
+
for (const [serverName, tools] of Object.entries(toolsByServer)) {
|
|
274
|
+
if (tools.length > 0) {
|
|
275
|
+
logInfo(`${serverName}: ${tools.join(', ')}`, { component: 'Bridge', server: serverName });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Create the executor using the codemode SDK pattern
|
|
279
|
+
const { executor, info: executorInfo } = await createExecutor(30000); // 30 second timeout
|
|
280
|
+
// Create the codemode tool using the codemode SDK
|
|
281
|
+
// Pass ToolDescriptor format (with Zod schemas and execute functions)
|
|
282
|
+
logInfo(`Creating codemode tool with ${totalToolCount} tools from ${serverConfigs.length} server(s)`, { component: 'Bridge' });
|
|
283
|
+
const codemodeTool = createCodeTool({
|
|
284
|
+
tools: allToolDescriptors,
|
|
285
|
+
executor,
|
|
286
|
+
// Let the SDK auto-generate description from available tools
|
|
287
|
+
});
|
|
288
|
+
// Adapt the AI SDK Tool to MCP protocol format and register it
|
|
289
|
+
// The adaptAISDKToolToMCP function handles the protocol conversion
|
|
290
|
+
await adaptAISDKToolToMCP(mcp, codemodeTool);
|
|
291
|
+
// Register the status tool — returns executor mode, upstream servers, and tool counts
|
|
292
|
+
mcp.registerTool("status", {
|
|
293
|
+
description: "Get the current status of the codemode bridge: executor mode, upstream server connections, and available tools.",
|
|
294
|
+
inputSchema: z.object({}).strict(),
|
|
295
|
+
}, async () => {
|
|
296
|
+
const servers = Object.entries(toolsByServer).map(([name, tools]) => ({
|
|
297
|
+
name,
|
|
298
|
+
toolCount: tools.length,
|
|
299
|
+
tools,
|
|
300
|
+
}));
|
|
301
|
+
const status = {
|
|
302
|
+
executor: {
|
|
303
|
+
type: executorInfo.type,
|
|
304
|
+
reason: executorInfo.reason,
|
|
305
|
+
timeout: executorInfo.timeout,
|
|
306
|
+
},
|
|
307
|
+
servers,
|
|
308
|
+
totalTools: totalToolCount,
|
|
309
|
+
};
|
|
310
|
+
return {
|
|
311
|
+
content: [{ type: "text", text: JSON.stringify(status, null, 2) }],
|
|
312
|
+
};
|
|
313
|
+
});
|
|
314
|
+
// Connect downstream MCP transport (what the client connects to)
|
|
315
|
+
const transport = new StdioServerTransport();
|
|
316
|
+
await mcp.connect(transport);
|
|
317
|
+
logInfo(`Ready on stdio transport`, { component: 'Bridge' });
|
|
318
|
+
logDebug(`Registering tool request handler`, {
|
|
319
|
+
component: 'Bridge'
|
|
320
|
+
});
|
|
321
|
+
logInfo(`Exposing 'eval' and 'status' tools`, { component: 'Bridge' });
|
|
322
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth2 Token Storage
|
|
3
|
+
*
|
|
4
|
+
* Persists OAuth tokens and client information to disk for reuse across sessions.
|
|
5
|
+
* Stored in ~/.config/codemode-bridge/mcp-tokens.json
|
|
6
|
+
*/
|
|
7
|
+
import type { OAuthTokens, OAuthClientInformationMixed } from '@modelcontextprotocol/sdk/shared/auth.js';
|
|
8
|
+
/**
|
|
9
|
+
* Manages OAuth token storage for MCP server connections
|
|
10
|
+
*/
|
|
11
|
+
export declare class TokenPersistence {
|
|
12
|
+
private configDir;
|
|
13
|
+
private tokenFile;
|
|
14
|
+
private storage;
|
|
15
|
+
constructor();
|
|
16
|
+
/**
|
|
17
|
+
* Load tokens from disk
|
|
18
|
+
*/
|
|
19
|
+
private loadStorage;
|
|
20
|
+
/**
|
|
21
|
+
* Save storage to disk
|
|
22
|
+
*/
|
|
23
|
+
private saveStorage;
|
|
24
|
+
/**
|
|
25
|
+
* Get stored client information for a server
|
|
26
|
+
*/
|
|
27
|
+
getClientInformation(serverUrl: string): OAuthClientInformationMixed | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Save client information for a server
|
|
30
|
+
*/
|
|
31
|
+
saveClientInformation(serverUrl: string, clientInfo: OAuthClientInformationMixed): void;
|
|
32
|
+
/**
|
|
33
|
+
* Get stored tokens for a server
|
|
34
|
+
*/
|
|
35
|
+
getTokens(serverUrl: string): OAuthTokens | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Save tokens for a server
|
|
38
|
+
*/
|
|
39
|
+
saveTokens(serverUrl: string, tokens: OAuthTokens): void;
|
|
40
|
+
/**
|
|
41
|
+
* Clear all tokens for a server (useful when revoked)
|
|
42
|
+
*/
|
|
43
|
+
clearTokens(serverUrl: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Clear all information for a server
|
|
46
|
+
*/
|
|
47
|
+
clearAll(serverUrl: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* Check if a server has stored tokens (and whether they're expired)
|
|
50
|
+
* Returns { exists: boolean, isExpired: boolean }
|
|
51
|
+
*/
|
|
52
|
+
getTokenStatus(serverUrl: string): {
|
|
53
|
+
exists: boolean;
|
|
54
|
+
isExpired: boolean;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export declare const tokenPersistence: TokenPersistence;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth2 Token Storage
|
|
3
|
+
*
|
|
4
|
+
* Persists OAuth tokens and client information to disk for reuse across sessions.
|
|
5
|
+
* Stored in ~/.config/codemode-bridge/mcp-tokens.json
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
/**
|
|
11
|
+
* Manages OAuth token storage for MCP server connections
|
|
12
|
+
*/
|
|
13
|
+
export class TokenPersistence {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.storage = {};
|
|
16
|
+
this.configDir = join(homedir(), '.config', 'codemode-bridge');
|
|
17
|
+
this.tokenFile = join(this.configDir, 'mcp-tokens.json');
|
|
18
|
+
this.loadStorage();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Load tokens from disk
|
|
22
|
+
*/
|
|
23
|
+
loadStorage() {
|
|
24
|
+
try {
|
|
25
|
+
if (existsSync(this.tokenFile)) {
|
|
26
|
+
const content = readFileSync(this.tokenFile, 'utf-8');
|
|
27
|
+
this.storage = JSON.parse(content);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
console.warn('Failed to load token storage:', error);
|
|
32
|
+
this.storage = {};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Save storage to disk
|
|
37
|
+
*/
|
|
38
|
+
saveStorage() {
|
|
39
|
+
try {
|
|
40
|
+
// Ensure directory exists
|
|
41
|
+
if (!existsSync(this.configDir)) {
|
|
42
|
+
mkdirSync(this.configDir, { recursive: true });
|
|
43
|
+
}
|
|
44
|
+
// Write tokens file
|
|
45
|
+
writeFileSync(this.tokenFile, JSON.stringify(this.storage, null, 2));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.warn('Failed to save token storage:', error);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Get stored client information for a server
|
|
53
|
+
*/
|
|
54
|
+
getClientInformation(serverUrl) {
|
|
55
|
+
return this.storage[serverUrl]?.clientInformation;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Save client information for a server
|
|
59
|
+
*/
|
|
60
|
+
saveClientInformation(serverUrl, clientInfo) {
|
|
61
|
+
if (!this.storage[serverUrl]) {
|
|
62
|
+
this.storage[serverUrl] = { lastUpdated: Date.now() };
|
|
63
|
+
}
|
|
64
|
+
this.storage[serverUrl].clientInformation = clientInfo;
|
|
65
|
+
this.storage[serverUrl].lastUpdated = Date.now();
|
|
66
|
+
this.saveStorage();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get stored tokens for a server
|
|
70
|
+
*/
|
|
71
|
+
getTokens(serverUrl) {
|
|
72
|
+
const info = this.storage[serverUrl];
|
|
73
|
+
if (!info?.tokens) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
// Check if tokens have expired
|
|
77
|
+
const tokens = info.tokens;
|
|
78
|
+
const expiresAt = info.lastUpdated + ((tokens.expires_in || 3600) * 1000);
|
|
79
|
+
if (expiresAt < Date.now()) {
|
|
80
|
+
// Tokens expired - don't return them
|
|
81
|
+
// The OAuth provider will request new ones
|
|
82
|
+
delete info.tokens;
|
|
83
|
+
this.saveStorage();
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
return tokens;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Save tokens for a server
|
|
90
|
+
*/
|
|
91
|
+
saveTokens(serverUrl, tokens) {
|
|
92
|
+
if (!this.storage[serverUrl]) {
|
|
93
|
+
this.storage[serverUrl] = { lastUpdated: Date.now() };
|
|
94
|
+
}
|
|
95
|
+
this.storage[serverUrl].tokens = tokens;
|
|
96
|
+
this.storage[serverUrl].lastUpdated = Date.now();
|
|
97
|
+
this.saveStorage();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Clear all tokens for a server (useful when revoked)
|
|
101
|
+
*/
|
|
102
|
+
clearTokens(serverUrl) {
|
|
103
|
+
if (this.storage[serverUrl]) {
|
|
104
|
+
delete this.storage[serverUrl].tokens;
|
|
105
|
+
this.saveStorage();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Clear all information for a server
|
|
110
|
+
*/
|
|
111
|
+
clearAll(serverUrl) {
|
|
112
|
+
delete this.storage[serverUrl];
|
|
113
|
+
this.saveStorage();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Check if a server has stored tokens (and whether they're expired)
|
|
117
|
+
* Returns { exists: boolean, isExpired: boolean }
|
|
118
|
+
*/
|
|
119
|
+
getTokenStatus(serverUrl) {
|
|
120
|
+
const info = this.storage[serverUrl];
|
|
121
|
+
if (!info?.tokens) {
|
|
122
|
+
return { exists: false, isExpired: false };
|
|
123
|
+
}
|
|
124
|
+
const tokens = info.tokens;
|
|
125
|
+
const expiresAt = info.lastUpdated + ((tokens.expires_in || 3600) * 1000);
|
|
126
|
+
const isExpired = expiresAt < Date.now();
|
|
127
|
+
return { exists: true, isExpired };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Singleton instance
|
|
131
|
+
export const tokenPersistence = new TokenPersistence();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility using Winston
|
|
3
|
+
*
|
|
4
|
+
* Provides structured logging with debug mode support
|
|
5
|
+
* All logs go to stderr to avoid interfering with JSON-RPC protocol on stdout
|
|
6
|
+
*/
|
|
7
|
+
import winston from 'winston';
|
|
8
|
+
/**
|
|
9
|
+
* Initialize the logger
|
|
10
|
+
*/
|
|
11
|
+
export declare function initializeLogger(debug?: boolean): void;
|
|
12
|
+
/**
|
|
13
|
+
* Get the logger instance
|
|
14
|
+
*/
|
|
15
|
+
export declare function getLogger(): winston.Logger;
|
|
16
|
+
/**
|
|
17
|
+
* Check if debug mode is enabled
|
|
18
|
+
*/
|
|
19
|
+
export declare function isDebugEnabled(): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Enable buffering of stderr output from stdio tools
|
|
22
|
+
* Useful for deferring tool output until after startup is complete
|
|
23
|
+
*/
|
|
24
|
+
export declare function enableStderrBuffering(): void;
|
|
25
|
+
/**
|
|
26
|
+
* Disable buffering and flush all buffered stderr messages
|
|
27
|
+
*/
|
|
28
|
+
export declare function flushStderrBuffer(): void;
|
|
29
|
+
/**
|
|
30
|
+
* Log an info message
|
|
31
|
+
*/
|
|
32
|
+
export declare function logInfo(message: string, meta?: Record<string, any>): void;
|
|
33
|
+
/**
|
|
34
|
+
* Log a debug message
|
|
35
|
+
*/
|
|
36
|
+
export declare function logDebug(message: string, meta?: Record<string, any>): void;
|
|
37
|
+
/**
|
|
38
|
+
* Log a warning message
|
|
39
|
+
*/
|
|
40
|
+
export declare function logWarn(message: string, meta?: Record<string, any>): void;
|
|
41
|
+
/**
|
|
42
|
+
* Log an error message
|
|
43
|
+
*/
|
|
44
|
+
export declare function logError(message: string, error?: Error | Record<string, any>): void;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility using Winston
|
|
3
|
+
*
|
|
4
|
+
* Provides structured logging with debug mode support
|
|
5
|
+
* All logs go to stderr to avoid interfering with JSON-RPC protocol on stdout
|
|
6
|
+
*/
|
|
7
|
+
import winston from 'winston';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
let logger;
|
|
10
|
+
let debugMode = false;
|
|
11
|
+
let stderrBufferingEnabled = false;
|
|
12
|
+
let stderrBuffer = [];
|
|
13
|
+
/**
|
|
14
|
+
* Initialize the logger
|
|
15
|
+
*/
|
|
16
|
+
export function initializeLogger(debug = false) {
|
|
17
|
+
debugMode = debug;
|
|
18
|
+
const level = debug ? 'debug' : 'info';
|
|
19
|
+
logger = winston.createLogger({
|
|
20
|
+
level,
|
|
21
|
+
format: winston.format.combine(winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), winston.format.errors({ stack: true }), winston.format.printf(({ level, message, timestamp, component, suppressEarly, ...meta }) => {
|
|
22
|
+
// Color the level based on severity
|
|
23
|
+
let coloredLevel;
|
|
24
|
+
switch (level.toUpperCase()) {
|
|
25
|
+
case 'ERROR':
|
|
26
|
+
coloredLevel = chalk.red(`[${level.toUpperCase()}]`);
|
|
27
|
+
break;
|
|
28
|
+
case 'WARN':
|
|
29
|
+
coloredLevel = chalk.yellow(`[${level.toUpperCase()}]`);
|
|
30
|
+
break;
|
|
31
|
+
case 'INFO':
|
|
32
|
+
coloredLevel = chalk.green(`[${level.toUpperCase()}]`);
|
|
33
|
+
break;
|
|
34
|
+
case 'DEBUG':
|
|
35
|
+
coloredLevel = chalk.blue(`[${level.toUpperCase()}]`);
|
|
36
|
+
break;
|
|
37
|
+
default:
|
|
38
|
+
coloredLevel = `[${level.toUpperCase()}]`;
|
|
39
|
+
}
|
|
40
|
+
// Build the prefix with timestamp (debug only)
|
|
41
|
+
const timestampStr = debug ? `[${timestamp}] ` : '';
|
|
42
|
+
// Add component prefix if provided (colored in cyan)
|
|
43
|
+
const componentStr = component ? ` ${chalk.cyan(`[${component}]`)}` : '';
|
|
44
|
+
// Include remaining metadata if present (excluding suppressEarly which is internal)
|
|
45
|
+
const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : '';
|
|
46
|
+
return `${timestampStr}${coloredLevel}${componentStr} ${message}${metaStr}`.trim();
|
|
47
|
+
})),
|
|
48
|
+
transports: [
|
|
49
|
+
// Always write to stderr to avoid interfering with stdout (used for JSON-RPC protocol)
|
|
50
|
+
new winston.transports.Stream({ stream: process.stderr }),
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get the logger instance
|
|
56
|
+
*/
|
|
57
|
+
export function getLogger() {
|
|
58
|
+
if (!logger) {
|
|
59
|
+
initializeLogger();
|
|
60
|
+
}
|
|
61
|
+
return logger;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if debug mode is enabled
|
|
65
|
+
*/
|
|
66
|
+
export function isDebugEnabled() {
|
|
67
|
+
return debugMode;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Enable buffering of stderr output from stdio tools
|
|
71
|
+
* Useful for deferring tool output until after startup is complete
|
|
72
|
+
*/
|
|
73
|
+
export function enableStderrBuffering() {
|
|
74
|
+
stderrBufferingEnabled = true;
|
|
75
|
+
stderrBuffer = [];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Disable buffering and flush all buffered stderr messages
|
|
79
|
+
*/
|
|
80
|
+
export function flushStderrBuffer() {
|
|
81
|
+
stderrBufferingEnabled = false;
|
|
82
|
+
const buffered = stderrBuffer;
|
|
83
|
+
stderrBuffer = [];
|
|
84
|
+
// Log all buffered messages directly without going through logInfo to avoid recursion
|
|
85
|
+
for (const { message, meta } of buffered) {
|
|
86
|
+
getLogger().info(message, meta);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Log an info message
|
|
91
|
+
*/
|
|
92
|
+
export function logInfo(message, meta) {
|
|
93
|
+
// Buffer logs marked with suppressEarly during startup if buffering is enabled
|
|
94
|
+
if (stderrBufferingEnabled && meta?.suppressEarly) {
|
|
95
|
+
stderrBuffer.push({ message, meta });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
getLogger().info(message, meta);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Log a debug message
|
|
103
|
+
*/
|
|
104
|
+
export function logDebug(message, meta) {
|
|
105
|
+
getLogger().debug(message, meta);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Log a warning message
|
|
109
|
+
*/
|
|
110
|
+
export function logWarn(message, meta) {
|
|
111
|
+
getLogger().warn(message, meta);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Log an error message
|
|
115
|
+
*/
|
|
116
|
+
export function logError(message, error) {
|
|
117
|
+
if (error instanceof Error) {
|
|
118
|
+
getLogger().error(message, { error: error.message, stack: error.stack });
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
getLogger().error(message, error);
|
|
122
|
+
}
|
|
123
|
+
}
|