Skip to main content
Glama

ChatGPT Apps EdgeOne Pages Starter

by siciyuan
httpTransport.ts7.54 kB
import type { Context } from 'hono' import { JSONRPCMessageSchema, isJSONRPCError, isJSONRPCRequest, isJSONRPCResponse, type JSONRPCError, type JSONRPCMessage, type JSONRPCResponse, type MessageExtraInfo, type RequestId, } from '@modelcontextprotocol/sdk/types.js' import type { Transport, TransportSendOptions } from '@modelcontextprotocol/sdk/shared/transport.js' import type { StatusCode } from 'hono/utils/http-status' type PendingEntry = { resolve: (message: JSONRPCResponse | JSONRPCError) => void reject: (error: Error) => void } type JsonRpcErrorPayload = { jsonrpc: '2.0' id: RequestId | null error: { code: number message: string } } const createJsonRpcError = (code: number, message: string, id: RequestId | null): JsonRpcErrorPayload => ({ jsonrpc: '2.0', id, error: { code, message, }, }) const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === 'object' && value !== null /** * Minimal HTTP transport for MCP servers running behind Hono. * * This implementation intentionally omits SSE support and responds synchronously * to POST requests with the JSON-RPC responses produced by the MCP server. */ export class HttpJsonTransport implements Transport { private started = false private pending = new Map<RequestId, PendingEntry>() sessionId: string | undefined onclose: (() => void) | undefined onerror: ((error: Error) => void) | undefined onmessage: ((message: JSONRPCMessage, extra?: MessageExtraInfo) => void) | undefined async start() { if (this.started) { throw new Error('Transport already started') } this.started = true } async handleRequest(ctx: Context, parsedBody?: unknown): Promise<Response> { switch (ctx.req.method) { case 'POST': return this.handlePostRequest(ctx, parsedBody) case 'OPTIONS': return this.handleOptionsRequest(ctx) default: return this.handleUnsupportedRequest(ctx) } } async close(): Promise<void> { this.pending.forEach((entry) => { entry.reject(new Error('Transport closed')) }) this.pending.clear() this.onclose?.() } async send(message: JSONRPCMessage, options?: TransportSendOptions): Promise<void> { let requestId = options?.relatedRequestId if (isJSONRPCResponse(message) || isJSONRPCError(message)) { requestId = message.id ?? requestId } if (requestId === undefined || requestId === null) { if (isJSONRPCResponse(message) || isJSONRPCError(message)) { throw new Error('HTTP transport requires request id to deliver responses') } // Notifications cannot be delivered without SSE support; ignore silently. return } const pending = this.pending.get(requestId) if (!pending) { if (isJSONRPCResponse(message) || isJSONRPCError(message)) { throw new Error(`No pending HTTP request for id ${String(requestId)}`) } return } if (!isJSONRPCResponse(message) && !isJSONRPCError(message)) { // Non-response messages cannot be delivered in HTTP mode. return } this.pending.delete(requestId) pending.resolve(message) } private async handlePostRequest(ctx: Context, parsedBody?: unknown): Promise<Response> { try { const accept = ctx.req.header('Accept') if (accept) { const normalized = accept.toLowerCase() if (!normalized.includes('application/json') && !normalized.includes('*/*')) { return this.jsonError( ctx, 406, -32000, 'Not Acceptable: application/json required in Accept header', ) } } const contentType = ctx.req.header('Content-Type')?.toLowerCase() if (!contentType || !contentType.includes('application/json')) { return this.jsonError( ctx, 415, -32000, 'Unsupported Media Type: Content-Type must be application/json', ) } const authInfo = ctx.get('auth') as MessageExtraInfo['authInfo'] const requestHeaders = Object.fromEntries(ctx.req.raw.headers.entries()) const requestInfo: MessageExtraInfo['requestInfo'] = { headers: requestHeaders, } as MessageExtraInfo['requestInfo'] let rawMessage = parsedBody if (rawMessage === undefined) { rawMessage = await ctx.req.json() } let messages: JSONRPCMessage[] if (Array.isArray(rawMessage)) { messages = rawMessage.map((msg) => JSONRPCMessageSchema.parse(msg)) } else { messages = [JSONRPCMessageSchema.parse(rawMessage)] } const requests = messages.filter(isJSONRPCRequest) if (requests.length === 0) { for (const message of messages) { this.onmessage?.(message, { authInfo, requestInfo }) } return ctx.newResponse(null, 202) } const responsePromises = requests.map((request) => { if (request.id === undefined) { throw new Error('JSON-RPC request is missing id') } return new Promise<JSONRPCResponse | JSONRPCError>((resolve, reject) => { this.pending.set(request.id, { resolve, reject }) }) }) for (const message of messages) { this.onmessage?.(message, { authInfo, requestInfo }) } const responses = await Promise.all(responsePromises) const payload: JSONRPCResponse | JSONRPCError | Array<JSONRPCResponse | JSONRPCError> = responses.length === 1 ? responses[0] : responses if (this.sessionId) { ctx.header('mcp-session-id', this.sessionId) } const negotiatedVersion = this.findProtocolVersion(payload) if (negotiatedVersion) { ctx.header('mcp-protocol-version', negotiatedVersion) } return ctx.json(payload) } catch (error) { const err = error instanceof Error ? error : new Error(String(error)) this.onerror?.(err) return this.jsonError(ctx, 400, -32700, 'Parse error', err) } } private handleOptionsRequest(ctx: Context): Response { const headers: HeadersInit = { Allow: 'POST, OPTIONS', } return ctx.newResponse(null, { status: 204, headers, }) } private handleUnsupportedRequest(ctx: Context): Response { return ctx.newResponse( JSON.stringify(createJsonRpcError(-32000, 'Method not allowed.', null)), 405, { Allow: 'POST, OPTIONS', 'Content-Type': 'application/json', }, ) } private jsonError(ctx: Context, status: StatusCode, code: number, message: string, error?: Error) { if (error) { this.onerror?.(error) } return ctx.newResponse(JSON.stringify(createJsonRpcError(code, message, null)), status, { 'Content-Type': 'application/json', }) } private findProtocolVersion( payload: JSONRPCResponse | JSONRPCError | Array<JSONRPCResponse | JSONRPCError>, ): string | undefined { const inspect = (item: JSONRPCResponse | JSONRPCError) => { if (!('result' in item)) { return undefined } if (isRecord(item.result) && typeof item.result.protocolVersion === 'string') { return item.result.protocolVersion } return undefined } if (Array.isArray(payload)) { for (const item of payload) { const version = inspect(item) if (version) { return version } } return undefined } return inspect(payload) } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/siciyuan/chatgpt-apps-edgeone-pages'

If you have feedback or need assistance with the MCP directory API, please join our Discord server