From 00b1d15cfd9b6a2342e53657a086db8a7e492e36 Mon Sep 17 00:00:00 2001 From: Minoru Mizutani Date: Tue, 29 Apr 2025 03:21:40 +0900 Subject: [PATCH] Implement rest part --- deno.json | 10 ++++++++-- implmentation_plan.md | 6 ++++-- src/client.ts | 29 +++++++++++++++-------------- src/lib/utils.ts | 41 ++++++++++++++++++++++++++++------------- 4 files changed, 55 insertions(+), 31 deletions(-) diff --git a/deno.json b/deno.json index f0b6335..32c3222 100644 --- a/deno.json +++ b/deno.json @@ -2,6 +2,10 @@ "name": "@legalforce/mcp-remote-deno", "version": "1.0.0", "description": "Deno wrapper for mcp-use proxy server", + "exports": { + ".": "./src/proxy.ts", + "./client": "./src/client.ts" + }, "publish": { "include": [ "dist/", @@ -10,8 +14,10 @@ ] }, "tasks": { - "start": "deno run --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/proxy.ts", - "dev": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/proxy.ts" + "proxy:start": "deno run --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/proxy.ts", + "proxy:watch": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/proxy.ts", + "client:start": "deno run --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts", + "client:watch": "deno run --watch --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts" }, "imports": { "std/": "https://deno.land/std@0.220.0/", diff --git a/implmentation_plan.md b/implmentation_plan.md index d11017e..4aa88cb 100644 --- a/implmentation_plan.md +++ b/implmentation_plan.md @@ -25,7 +25,9 @@ Here is a plan to transform your Node.js CLI package into a Deno CLI project, fo - [x] Decide whether to keep `package.json`. It's not used by Deno for dependencies but can be useful for metadata (name, version, description). If kept, ensure it doesn't cause confusion. - [x] Remove `tsconfig.json` if all necessary compiler options are migrated to `deno.json`. Linters/editors might still pick it up, so consider keeping it for tooling compatibility if needed, but `deno.json` takes precedence for Deno itself. 5. **Testing:** - - [ ] Run the main task using `deno task start `. - - [ ] Thoroughly test the CLI's functionality to ensure it behaves identically to the original Node.js version. Pay close attention to areas involving file system access, network requests, environment variables, and process management. + - [x] Added client.ts as an additional CLI entrypoint + - [x] Added client task to deno.json + - [x] Run the main task using `deno task proxy:start `. + - [x] Thoroughly test the CLI's functionality to ensure it behaves identically to the original Node.js version. Pay close attention to areas involving file system access, network requests, environment variables, and process management. This plan prioritizes modifying the existing TypeScript code minimally while adapting the project structure and configuration for Deno. We will start by modifying `deno.json` and `src/proxy.ts`. diff --git a/src/client.ts b/src/client.ts index d620884..d1f9298 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,22 +1,23 @@ #!/usr/bin/env node +/// /** * MCP Client with OAuth support * A command-line client that connects to an MCP server using SSE with OAuth authentication. * - * Run with: npx tsx client.ts https://example.remote/server [callback-port] + * Run with: deno run --allow-net --allow-env --allow-read --allow-run --allow-sys --allow-ffi src/client.ts https://example.remote/server [callback-port] * * If callback-port is not specified, an available port will be automatically selected. */ -import { EventEmitter } from 'events' -import { Client } from '@modelcontextprotocol/sdk/client/index.js' -import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js' -import { ListResourcesResultSchema, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' -import { UnauthorizedError } from '@modelcontextprotocol/sdk/client/auth.js' -import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider' -import { parseCommandLineArgs, setupSignalHandlers, log, MCP_REMOTE_VERSION, getServerUrlHash } from './lib/utils' -import { coordinateAuth } from './lib/coordination' +import { EventEmitter } from 'node:events' +import { Client } from 'npm:@modelcontextprotocol/sdk/client/index.js' +import { SSEClientTransport } from 'npm:@modelcontextprotocol/sdk/client/sse.js' +import { ListResourcesResultSchema, ListToolsResultSchema } from 'npm:@modelcontextprotocol/sdk/types.js' +import { UnauthorizedError } from 'npm:@modelcontextprotocol/sdk/client/auth.js' +import { NodeOAuthClientProvider } from './lib/node-oauth-client-provider.ts' +import { parseCommandLineArgs, setupSignalHandlers, log, MCP_REMOTE_VERSION, getServerUrlHash } from './lib/utils.ts' +import { coordinateAuth } from './lib/coordination.ts' /** * Main function to run the client @@ -73,7 +74,7 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor transport.onclose = () => { log('Connection closed.') - process.exit(0) + Deno.exit(0) } return transport } @@ -124,12 +125,12 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor } catch (authError) { log('Authorization error:', authError) server.close() - process.exit(1) + Deno.exit(1) } } else { log('Connection error:', error) server.close() - process.exit(1) + Deno.exit(1) } } @@ -155,11 +156,11 @@ async function runClient(serverUrl: string, callbackPort: number, headers: Recor } // Parse command-line arguments and run the client -parseCommandLineArgs(process.argv.slice(2), 3333, 'Usage: npx tsx client.ts [callback-port]') +parseCommandLineArgs(Deno.args, 3333, 'Usage: deno run src/client.ts [callback-port]') .then(({ serverUrl, callbackPort, headers }) => { return runClient(serverUrl, callbackPort, headers) }) .catch((error) => { console.error('Fatal error:', error) - process.exit(1) + Deno.exit(1) }) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 1f47c13..a4da17a 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -78,7 +78,7 @@ export async function connectToRemoteServer( authProvider: OAuthClientProvider, headers: Record, waitForAuthCode: () => Promise, - skipBrowserAuth: boolean = false, + skipBrowserAuth = false, ): Promise { log(`[${pid}] Connecting to remote server: ${serverUrl}`) const url = new URL(serverUrl) @@ -282,6 +282,12 @@ export async function findAvailablePort(preferredPort?: number): Promise * @returns A promise that resolves to an object with parsed serverUrl, callbackPort and headers */ export async function parseCommandLineArgs(args: string[], defaultPort: number, usage: string) { + // Check for help flag + if (args.includes('--help') || args.includes('-h')) { + log(usage) + Deno.exit(0) + } + // Process headers const headers: Record = {} args.forEach((arg, i) => { @@ -298,21 +304,21 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, }) const serverUrl = args[0] - const specifiedPort = args[1] ? parseInt(args[1]) : undefined + const specifiedPort = args[1] ? Number.parseInt(args[1], 10) : undefined const allowHttp = args.includes('--allow-http') if (!serverUrl) { log(usage) - process.exit(1) + Deno.exit(1) } const url = new URL(serverUrl) const isLocalhost = (url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.protocol === 'http:' - if (!(url.protocol == 'https:' || isLocalhost || allowHttp)) { + if (!(url.protocol === 'https:' || isLocalhost || allowHttp)) { log('Error: Non-HTTPS URLs are only allowed for localhost or when --allow-http flag is provided') log(usage) - process.exit(1) + Deno.exit(1) } // Use the specified port, or find an available one @@ -331,15 +337,15 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, // example `Authorization: Bearer ${TOKEN}` will read process.env.TOKEN for (const [key, value] of Object.entries(headers)) { headers[key] = value.replace(/\$\{([^}]+)}/g, (match, envVarName) => { - const envVarValue = process.env[envVarName] + const envVarValue = Deno.env.get(envVarName) if (envVarValue !== undefined) { log(`Replacing ${match} with environment value in header '${key}'`) return envVarValue - } else { - log(`Warning: Environment variable '${envVarName}' not found for header '${key}'.`) - return '' } + + log(`Warning: Environment variable '${envVarName}' not found for header '${key}'.`) + return '' }) } @@ -351,14 +357,23 @@ export async function parseCommandLineArgs(args: string[], defaultPort: number, * @param cleanup Cleanup function to run on shutdown */ export function setupSignalHandlers(cleanup: () => Promise) { - process.on('SIGINT', async () => { + Deno.addSignalListener("SIGINT", async () => { log('\nShutting down...') await cleanup() - process.exit(0) + Deno.exit(0) }) - // Keep the process alive - process.stdin.resume() + // For SIGTERM + try { + Deno.addSignalListener("SIGTERM", async () => { + log('\nReceived SIGTERM. Shutting down...') + await cleanup() + Deno.exit(0) + }) + } catch (e) { + // SIGTERM might not be available on all platforms + log('SIGTERM handler not available on this platform') + } } /**