Implement rest part
This commit is contained in:
parent
b0216017a2
commit
00b1d15cfd
4 changed files with 55 additions and 31 deletions
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue