Implement rest part

This commit is contained in:
Minoru Mizutani 2025-04-29 03:21:40 +09:00
parent b0216017a2
commit 00b1d15cfd
No known key found for this signature in database
4 changed files with 55 additions and 31 deletions

View file

@ -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/",

View file

@ -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 <args...>`.
- [ ] 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 <args...>`.
- [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`.

View file

@ -1,22 +1,23 @@
#!/usr/bin/env node
/// <reference lib="deno.ns" />
/**
* 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 <https://server-url> [callback-port]')
parseCommandLineArgs(Deno.args, 3333, 'Usage: deno run src/client.ts <https://server-url> [callback-port]')
.then(({ serverUrl, callbackPort, headers }) => {
return runClient(serverUrl, callbackPort, headers)
})
.catch((error) => {
console.error('Fatal error:', error)
process.exit(1)
Deno.exit(1)
})

View file

@ -78,7 +78,7 @@ export async function connectToRemoteServer(
authProvider: OAuthClientProvider,
headers: Record<string, string>,
waitForAuthCode: () => Promise<string>,
skipBrowserAuth: boolean = false,
skipBrowserAuth = false,
): Promise<SSEClientTransport> {
log(`[${pid}] Connecting to remote server: ${serverUrl}`)
const url = new URL(serverUrl)
@ -282,6 +282,12 @@ export async function findAvailablePort(preferredPort?: number): Promise<number>
* @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<string, string> = {}
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 ''
}
})
}
@ -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<void>) {
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')
}
}
/**