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

@ -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 ''
}
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')
}
}
/**