Refactor implementation to replace Express with native Deno HTTP server and add cross-platform URL opening functionality. Update implementation plan to reflect completed tasks for dependency management.
This commit is contained in:
parent
4394c0773d
commit
2bbcaf7963
5 changed files with 232 additions and 18 deletions
|
@ -35,8 +35,8 @@ Here is a plan to transform your Node.js CLI package into a Deno CLI project, fo
|
||||||
- [x] Improve type definitions, especially for external library interactions (e.g., express request/response types if kept).
|
- [x] Improve type definitions, especially for external library interactions (e.g., express request/response types if kept).
|
||||||
- [x] Run `deno fmt` to ensure consistent code formatting.
|
- [x] Run `deno fmt` to ensure consistent code formatting.
|
||||||
7. **Improve Dependency Management:**
|
7. **Improve Dependency Management:**
|
||||||
- [ ] Evaluate replacing `npm:express` with a native Deno HTTP server solution (e.g., `Deno.serve` or from `std/http`).
|
- [x] Evaluate replacing `npm:express` with a native Deno HTTP server solution (e.g., `Deno.serve` or from `std/http`).
|
||||||
- [ ] Evaluate replacing `npm:open` with a Deno equivalent or platform-specific commands.
|
- [x] Evaluate replacing `npm:open` with a Deno equivalent or platform-specific commands.
|
||||||
8. **Implement Testing:**
|
8. **Implement Testing:**
|
||||||
- [ ] Add unit tests for key utility functions (e.g., in `utils.ts`, `mcp-auth-config.ts`).
|
- [ ] Add unit tests for key utility functions (e.g., in `utils.ts`, `mcp-auth-config.ts`).
|
||||||
- [ ] Add integration tests for the core proxy (`proxy.ts`) and client (`client.ts`) functionality.
|
- [ ] Add integration tests for the core proxy (`proxy.ts`) and client (`client.ts`) functionality.
|
||||||
|
|
144
src/lib/deno-http-server.ts
Normal file
144
src/lib/deno-http-server.ts
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
import type { Server } from "node:http";
|
||||||
|
|
||||||
|
// Simple type definitions for our server
|
||||||
|
interface RequestLike {
|
||||||
|
query: Record<string, string>;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple HTTP server using Deno's native HTTP server capabilities
|
||||||
|
* that mimics the Express API for our specific use case
|
||||||
|
*/
|
||||||
|
export class DenoHttpServer {
|
||||||
|
private server: Deno.HttpServer | null = null;
|
||||||
|
private routes: Map<string, (req: Request) => Promise<Response> | Response> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a GET route handler
|
||||||
|
* @param path The path to handle
|
||||||
|
* @param handler The handler function
|
||||||
|
*/
|
||||||
|
get(path: string, handler: (req: RequestLike, res: ResponseBuilder) => void): void {
|
||||||
|
this.routes.set(path, async (request: Request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const searchParams = url.searchParams;
|
||||||
|
const responseBuilder = new ResponseBuilder();
|
||||||
|
|
||||||
|
// Create a simple request object that mimics Express req
|
||||||
|
const query: Record<string, string> = {};
|
||||||
|
for (const [key, value] of searchParams) {
|
||||||
|
query[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const req: RequestLike = {
|
||||||
|
query,
|
||||||
|
path: url.pathname,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call the handler with our simplified req/res objects
|
||||||
|
handler(req, responseBuilder);
|
||||||
|
|
||||||
|
// Wait for the response to be ready (in case of async operations)
|
||||||
|
return await responseBuilder.getResponse();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the server listening on the specified port
|
||||||
|
* @param port The port to listen on
|
||||||
|
* @param callback Optional callback when server is ready
|
||||||
|
*/
|
||||||
|
listen(port: number, callback?: () => void): Server {
|
||||||
|
this.server = Deno.serve({
|
||||||
|
port,
|
||||||
|
onListen: callback ? () => callback() : undefined,
|
||||||
|
handler: async (request: Request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const path = url.pathname;
|
||||||
|
|
||||||
|
// Find the route handler
|
||||||
|
const handler = this.routes.get(path);
|
||||||
|
if (handler) {
|
||||||
|
return await handler(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route not found
|
||||||
|
return new Response("Not Found", { status: 404 });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a dummy server object that mimics Node's HTTP server
|
||||||
|
// This is needed to maintain API compatibility
|
||||||
|
return {
|
||||||
|
close: () => this.close(),
|
||||||
|
} as unknown as Server;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the server
|
||||||
|
*/
|
||||||
|
close(): Promise<void> {
|
||||||
|
if (this.server) {
|
||||||
|
return this.server.shutdown();
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response builder class that mimics Express Response
|
||||||
|
*/
|
||||||
|
export class ResponseBuilder {
|
||||||
|
private statusCode = 200;
|
||||||
|
private body: string | null = null;
|
||||||
|
private responsePromise: Promise<Response>;
|
||||||
|
private resolveResponse!: (response: Response) => void;
|
||||||
|
headersSent = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Create a promise that will be resolved when the response is ready
|
||||||
|
this.responsePromise = new Promise((resolve) => {
|
||||||
|
this.resolveResponse = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the HTTP status code
|
||||||
|
* @param code HTTP status code
|
||||||
|
* @returns this instance for chaining
|
||||||
|
*/
|
||||||
|
status(code: number): ResponseBuilder {
|
||||||
|
this.statusCode = code;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a response
|
||||||
|
* @param data The response data
|
||||||
|
*/
|
||||||
|
send(data: string): void {
|
||||||
|
if (this.headersSent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.headersSent = true;
|
||||||
|
this.body = data;
|
||||||
|
this.resolveResponse(new Response(this.body, { status: this.statusCode }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the response promise
|
||||||
|
* @returns Promise that resolves to the final Response
|
||||||
|
*/
|
||||||
|
getResponse(): Promise<Response> {
|
||||||
|
return this.responsePromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create and return a new HTTP server instance
|
||||||
|
* @returns A new HTTP server instance
|
||||||
|
*/
|
||||||
|
export default function createServer(): DenoHttpServer {
|
||||||
|
return new DenoHttpServer();
|
||||||
|
}
|
70
src/lib/deno-open.ts
Normal file
70
src/lib/deno-open.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
/**
|
||||||
|
* Opens a URL in the default browser.
|
||||||
|
* This is a cross-platform implementation using Deno subprocess API
|
||||||
|
* @param url The URL to open
|
||||||
|
* @returns A promise that resolves when the command has been executed
|
||||||
|
*/
|
||||||
|
export default async function open(url: string): Promise<void> {
|
||||||
|
let command: string[];
|
||||||
|
const isWindows = Deno.build.os === "windows";
|
||||||
|
const isMac = Deno.build.os === "darwin";
|
||||||
|
const isLinux = Deno.build.os === "linux";
|
||||||
|
|
||||||
|
if (isWindows) {
|
||||||
|
command = ["cmd", "/c", "start", "", url];
|
||||||
|
} else if (isMac) {
|
||||||
|
command = ["open", url];
|
||||||
|
} else if (isLinux) {
|
||||||
|
// On Linux, try several common browser-opener commands
|
||||||
|
const linuxCommands = [
|
||||||
|
["xdg-open", url],
|
||||||
|
["gnome-open", url],
|
||||||
|
["kde-open", url],
|
||||||
|
["wslview", url] // For Windows Subsystem for Linux
|
||||||
|
];
|
||||||
|
|
||||||
|
// Try each command in order until one succeeds
|
||||||
|
for (const cmd of linuxCommands) {
|
||||||
|
try {
|
||||||
|
const process = new Deno.Command(cmd[0], {
|
||||||
|
args: cmd.slice(1),
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null"
|
||||||
|
}).spawn();
|
||||||
|
|
||||||
|
const status = await process.status;
|
||||||
|
|
||||||
|
if (status.success) {
|
||||||
|
return; // Command succeeded, so exit the function
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If this command fails, try the next one
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, none of the commands worked
|
||||||
|
throw new Error("Could not open browser on Linux. Please open URL manually.");
|
||||||
|
} else {
|
||||||
|
throw new Error(`Unsupported platform: ${Deno.build.os}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Windows and Mac, execute the chosen command
|
||||||
|
if (isWindows || isMac) {
|
||||||
|
try {
|
||||||
|
const process = new Deno.Command(command[0], {
|
||||||
|
args: command.slice(1),
|
||||||
|
stdout: "null",
|
||||||
|
stderr: "null"
|
||||||
|
}).spawn();
|
||||||
|
|
||||||
|
const status = await process.status;
|
||||||
|
|
||||||
|
if (!status.success) {
|
||||||
|
throw new Error(`Failed to open ${url} in browser`);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
throw new Error(`Failed to open ${url} in browser: ${errorMessage}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import open from "npm:open";
|
|
||||||
import type { OAuthClientProvider } from "npm:@modelcontextprotocol/sdk/client/auth.js";
|
import type { OAuthClientProvider } from "npm:@modelcontextprotocol/sdk/client/auth.js";
|
||||||
import "npm:@modelcontextprotocol/sdk/client/auth.js";
|
import "npm:@modelcontextprotocol/sdk/client/auth.js";
|
||||||
import {
|
import {
|
||||||
|
@ -18,6 +17,7 @@ import {
|
||||||
writeTextFile,
|
writeTextFile,
|
||||||
} from "./mcp-auth-config.ts";
|
} from "./mcp-auth-config.ts";
|
||||||
import { getServerUrlHash, log, MCP_REMOTE_VERSION } from "./utils.ts";
|
import { getServerUrlHash, log, MCP_REMOTE_VERSION } from "./utils.ts";
|
||||||
|
import open from "./deno-open.ts";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements the OAuthClientProvider interface for Node.js environments.
|
* Implements the OAuthClientProvider interface for Node.js environments.
|
||||||
|
|
|
@ -5,9 +5,9 @@ import {
|
||||||
import { SSEClientTransport } from "npm:@modelcontextprotocol/sdk/client/sse.js";
|
import { SSEClientTransport } from "npm:@modelcontextprotocol/sdk/client/sse.js";
|
||||||
import type { Transport } from "npm:@modelcontextprotocol/sdk/shared/transport.js";
|
import type { Transport } from "npm:@modelcontextprotocol/sdk/shared/transport.js";
|
||||||
import type { OAuthCallbackServerOptions } from "./types.ts";
|
import type { OAuthCallbackServerOptions } from "./types.ts";
|
||||||
import express from "npm:express";
|
|
||||||
import net from "node:net";
|
import net from "node:net";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
|
import createServer from "./deno-http-server.ts";
|
||||||
|
|
||||||
// Package version from deno.json (set a constant for now)
|
// Package version from deno.json (set a constant for now)
|
||||||
export const MCP_REMOTE_VERSION = "1.0.0"; // TODO: Find better way to get version in Deno
|
export const MCP_REMOTE_VERSION = "1.0.0"; // TODO: Find better way to get version in Deno
|
||||||
|
@ -160,7 +160,18 @@ export async function connectToRemoteServer(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up an Express server to handle OAuth callbacks
|
* Sets up an HTTP server to handle OAuth callbacks
|
||||||
|
* @param options The server options
|
||||||
|
* @returns An object with the server, authCode, and waitForAuthCode function
|
||||||
|
*/
|
||||||
|
export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) {
|
||||||
|
const { server, authCode, waitForAuthCode } =
|
||||||
|
setupOAuthCallbackServerWithLongPoll(options);
|
||||||
|
return { server, authCode, waitForAuthCode };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets up an HTTP server to handle OAuth callbacks
|
||||||
* @param options The server options
|
* @param options The server options
|
||||||
* @returns An object with the server, authCode, and waitForAuthCode function
|
* @returns An object with the server, authCode, and waitForAuthCode function
|
||||||
*/
|
*/
|
||||||
|
@ -168,7 +179,7 @@ export function setupOAuthCallbackServerWithLongPoll(
|
||||||
options: OAuthCallbackServerOptions,
|
options: OAuthCallbackServerOptions,
|
||||||
) {
|
) {
|
||||||
let authCode: string | null = null;
|
let authCode: string | null = null;
|
||||||
const app = express();
|
const app = createServer();
|
||||||
|
|
||||||
// Create a promise to track when auth is completed
|
// Create a promise to track when auth is completed
|
||||||
let authCompletedResolve: (code: string) => void;
|
let authCompletedResolve: (code: string) => void;
|
||||||
|
@ -218,7 +229,7 @@ export function setupOAuthCallbackServerWithLongPoll(
|
||||||
|
|
||||||
// OAuth callback endpoint
|
// OAuth callback endpoint
|
||||||
app.get(options.path, (req, res) => {
|
app.get(options.path, (req, res) => {
|
||||||
const code = req.query.code as string | undefined;
|
const code = req.query.code;
|
||||||
if (!code) {
|
if (!code) {
|
||||||
res.status(400).send("Error: No authorization code received");
|
res.status(400).send("Error: No authorization code received");
|
||||||
return;
|
return;
|
||||||
|
@ -256,17 +267,6 @@ export function setupOAuthCallbackServerWithLongPoll(
|
||||||
return { server, authCode, waitForAuthCode, authCompletedPromise };
|
return { server, authCode, waitForAuthCode, authCompletedPromise };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets up an Express server to handle OAuth callbacks
|
|
||||||
* @param options The server options
|
|
||||||
* @returns An object with the server, authCode, and waitForAuthCode function
|
|
||||||
*/
|
|
||||||
export function setupOAuthCallbackServer(options: OAuthCallbackServerOptions) {
|
|
||||||
const { server, authCode, waitForAuthCode } =
|
|
||||||
setupOAuthCallbackServerWithLongPoll(options);
|
|
||||||
return { server, authCode, waitForAuthCode };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds an available port on the local machine
|
* Finds an available port on the local machine
|
||||||
* @param preferredPort Optional preferred port to try first
|
* @param preferredPort Optional preferred port to try first
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue