Skip to content

Commit

Permalink
feat: add PoC of HTTP client transport for TypeScript
Browse files Browse the repository at this point in the history
This client transport is a stateless transport that uses JSONRPC
over HTTP POST, foregoing stateful communication and therefore
avoiding some of the complexity of the SSE transport (and the
need for persistent connections). It does mean that server-to-client
communication is not possible.

This transport is also implemented in Python in [this PR], and
the TypeScript client has been tested against the Python server.

[this PR]: grafana/mcp-grafana#24
  • Loading branch information
sd2k committed Feb 14, 2025
1 parent 423b62b commit 52872c5
Show file tree
Hide file tree
Showing 2 changed files with 60 additions and 1 deletion.
6 changes: 5 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import WebSocket from "ws";

import express from "express";
import { Client } from "./client/index.js";
import { HTTPClientTransport } from "./client/http.js";
import { SSEClientTransport } from "./client/sse.js";
import { StdioClientTransport } from "./client/stdio.js";
import { WebSocketClientTransport } from "./client/websocket.js";
Expand Down Expand Up @@ -36,7 +37,10 @@ async function runClient(url_or_command: string, args: string[]) {
}

if (url?.protocol === "http:" || url?.protocol === "https:") {
clientTransport = new SSEClientTransport(new URL(url_or_command));
clientTransport = new HTTPClientTransport(new URL(url_or_command));
} else if (url?.protocol === "http+sse:" || url?.protocol === "https+sse:" || url?.protocol === "wss:") {
url = new URL(url.toString().replace("+sse", ""));
clientTransport = new SSEClientTransport(url);
} else if (url?.protocol === "ws:" || url?.protocol === "wss:") {
clientTransport = new WebSocketClientTransport(new URL(url_or_command));
} else {
Expand Down
55 changes: 55 additions & 0 deletions src/client/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Transport } from "../shared/transport.js";
import { JSONRPCMessage, JSONRPCMessageSchema } from "../types.js";


export class HTTPClientTransport implements Transport {
private _url: URL;
private _requestInit?: RequestInit;
private _abortController?: AbortController;

onclose?: () => void;
onerror?: (error: Error) => void;
onmessage?: (message: JSONRPCMessage) => void;

constructor(url: URL, requestInit?: RequestInit) {
this._url = url;
this._requestInit = requestInit;
}

async start(): Promise<void> {
this._abortController = new AbortController();
}

async send(message: JSONRPCMessage): Promise<void> {
try {
const headers = new Headers(this._requestInit?.headers);
headers.set("Content-Type", "application/json");
const init: RequestInit = {
...this._requestInit,
method: "POST",
body: JSON.stringify(message),
signal: this._abortController?.signal,
};
const response = await fetch(this._url, init);

if (!response.ok) {
const text = await response.text().catch(() => null);
throw new Error(`Error POSTing to URL (HTTP ${response.status}): ${text}`);
}

let responseMessage: JSONRPCMessage;
const responseJson = await response.json();
responseMessage = JSONRPCMessageSchema.parse(responseJson);

this.onmessage?.(responseMessage);
} catch (error) {
this.onerror?.(error as Error);
return;
}
}

async close(): Promise<void> {
this._abortController?.abort();
this.onclose?.();
}
}

0 comments on commit 52872c5

Please # to comment.