feat(mcp): add stdio host for local clients (#1102)
* feat(mcp): add stdio host for local clients * feat(mcp): add Streamable HTTP host and image Long-lived HTTP entry for Docker and other process hosts, reusing createServer(). Dedicated mcp/Dockerfile; compose service beside api. * fix(mcp): stdio launcher cwd/silent and HTTP session bounds Pin bun --cwd so bunfig loads. Silence bun run. Require Bearer on HTTP. Idle-expire and cap in-memory MCP sessions. * fix(mcp): re-check bearer on established HTTP sessions Session lookup returned early without Authorization, so a missing or wrong token still 200'd after initialize. Bind each session to the init key and 401 on mismatch. * fix: nit cleaning claude command --------- Co-authored-by: ajspig <dragon@monstercode.com>
This commit is contained in:
parent
b573a84806
commit
2ad56a4d71
|
|
@ -80,6 +80,34 @@ services:
|
|||
required: false
|
||||
restart: unless-stopped
|
||||
|
||||
mcp:
|
||||
build:
|
||||
context: ./mcp
|
||||
dockerfile: Dockerfile
|
||||
depends_on:
|
||||
api:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
environment:
|
||||
- HONCHO_API_URL=http://api:8000
|
||||
env_file:
|
||||
- path: .env
|
||||
required: false
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
"CMD",
|
||||
"bun",
|
||||
"-e",
|
||||
"fetch('http://127.0.0.1:3000/health').then((r)=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))",
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
restart: unless-stopped
|
||||
|
||||
database:
|
||||
image: pgvector/pgvector:pg15
|
||||
restart: unless-stopped
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
node_modules
|
||||
.wrangler
|
||||
.dev.vars
|
||||
.env
|
||||
.env.*
|
||||
*.log
|
||||
dist
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
FROM oven/bun:1.2
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN chown bun:bun /app
|
||||
USER bun
|
||||
|
||||
COPY --chown=bun:bun package.json bun.lock bunfig.toml tsconfig.json ./
|
||||
COPY --chown=bun:bun instructions.md ./
|
||||
COPY --chown=bun:bun src ./src
|
||||
|
||||
RUN bun install --frozen-lockfile --production
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
ENV PORT=3000
|
||||
ENV HOST=0.0.0.0
|
||||
|
||||
CMD ["bun", "src/http.ts"]
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# Honcho MCP Server
|
||||
|
||||
A Cloudflare Worker that implements the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) for [Honcho](https://honcho.dev), providing AI memory and personalization tools to LLM clients like Claude Desktop.
|
||||
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for [Honcho](https://honcho.dev). The hosted path is a Cloudflare Worker; the same tools also run over stdio and over Streamable HTTP (`bun src/http.ts`) for Docker and other long-lived process hosts.
|
||||
|
||||
## Quickstart: Use the Hosted Server
|
||||
|
||||
|
|
@ -45,6 +45,8 @@ Every workspace-scoped tool takes a `workspace_id` argument. If you set `X-Honch
|
|||
```
|
||||
src/
|
||||
index.ts # Worker entry point — parse config, delegate to MCP handler
|
||||
stdio.ts # Local stdio host (bun src/stdio.ts)
|
||||
http.ts # Streamable HTTP host (bun src/http.ts / Docker)
|
||||
server.ts # createServer() — registers all tools on an McpServer
|
||||
config.ts # HonchoConfig, parseConfig(), createClientFactory()
|
||||
types.ts # ToolContext, result helpers
|
||||
|
|
@ -64,25 +66,75 @@ Built on:
|
|||
|
||||
## Self-Hosted Honcho
|
||||
|
||||
If you run Honcho yourself (for privacy, latency, or offline use), deploy the
|
||||
MCP Worker alongside your instance and set `HONCHO_API_URL` in its
|
||||
environment.
|
||||
If you run Honcho yourself, point this server at it with `HONCHO_API_URL`.
|
||||
When unset, requests go to `https://api.honcho.dev`.
|
||||
|
||||
**Local dev (`bun run dev`):** create `mcp/.dev.vars`:
|
||||
**Cloudflare Worker (`bun run dev` / `bun run deploy`):** create `mcp/.dev.vars`:
|
||||
|
||||
```
|
||||
HONCHO_API_URL=http://127.0.0.1:28000
|
||||
```
|
||||
|
||||
**Deployed Worker:**
|
||||
For a deployed Worker: `wrangler secret put HONCHO_API_URL`.
|
||||
|
||||
## HTTP host
|
||||
|
||||
For Docker or any platform that runs a long-lived process, use the Streamable
|
||||
HTTP entry instead of the Worker. Clients keep the same `mcp-remote` shape as
|
||||
`https://mcp.honcho.dev`. Sessions live in process memory — run one instance.
|
||||
|
||||
```bash
|
||||
wrangler secret put HONCHO_API_URL
|
||||
# paste your URL when prompted
|
||||
cd mcp && bun install
|
||||
HONCHO_API_URL=http://127.0.0.1:8000 bun run http
|
||||
```
|
||||
|
||||
When `HONCHO_API_URL` is unset the Worker routes to `https://api.honcho.dev`,
|
||||
so this change is backward-compatible.
|
||||
```bash
|
||||
bunx mcp-remote http://127.0.0.1:3000 \
|
||||
--header "Authorization:Bearer <key>"
|
||||
```
|
||||
|
||||
Auth is the `Authorization: Bearer` header (same as the Worker). Established
|
||||
sessions still require that same bearer. Optional `X-Honcho-Workspace-ID`
|
||||
fills `workspace_id` when the tool argument is omitted.
|
||||
|
||||
`HOST` defaults to `0.0.0.0`, `PORT` to `3000`. `GET /health` is unauthenticated.
|
||||
MCP is served at `/` and `/mcp`. Idle sessions expire after
|
||||
`MCP_SESSION_IDLE_MS` (default 30 minutes); `MCP_SESSION_MAX` (default 128)
|
||||
caps concurrent sessions.
|
||||
|
||||
A platform start command is `bun src/http.ts` (or `bun run http` from `mcp/`).
|
||||
This repo does not ship a `vercel.json`; serverless replicas do not share the
|
||||
in-memory session map.
|
||||
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
docker build -f mcp/Dockerfile -t honcho-mcp mcp
|
||||
docker run --rm -p 3000:3000 \
|
||||
-e HONCHO_API_URL=http://host.docker.internal:8000 \
|
||||
honcho-mcp
|
||||
```
|
||||
|
||||
`docker-compose.yml.example` includes an `mcp` service beside `api` and
|
||||
`deriver` (`HONCHO_API_URL=http://api:8000`, port `127.0.0.1:3000`).
|
||||
|
||||
## Local stdio
|
||||
|
||||
For a local Honcho instance, or any MCP client that spawns a process, run the
|
||||
stdio host. `--cwd` loads `mcp/bunfig.toml` (Markdown loader) from this package.
|
||||
|
||||
```bash
|
||||
cd mcp && bun install
|
||||
|
||||
claude mcp add honcho \
|
||||
-e HONCHO_API_KEY=hch-your-key-here \
|
||||
-e HONCHO_API_URL=http://127.0.0.1:28000 \
|
||||
-e HONCHO_WORKSPACE_ID=my-workspace \
|
||||
-- bun --cwd "$(pwd)" src/stdio.ts
|
||||
```
|
||||
|
||||
`HONCHO_API_URL` defaults to `https://api.honcho.dev`. `HONCHO_WORKSPACE_ID` is
|
||||
optional; without it, pass `workspace_id` on each tool call.
|
||||
|
||||
## Development
|
||||
|
||||
|
|
@ -106,6 +158,8 @@ bun run tsc --noEmit
|
|||
|
||||
### Test locally
|
||||
|
||||
Worker (`bun dev`, port 8787) or HTTP host (`bun run http`, port 3000):
|
||||
|
||||
```bash
|
||||
bunx mcp-remote http://localhost:8787 \
|
||||
--header "Authorization:Bearer <key>"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
[loader]
|
||||
".md" = "text"
|
||||
|
||||
[run]
|
||||
silent = true
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "honcho-mcp",
|
||||
"version": "3.0.0",
|
||||
"description": "Honcho MCP Server — Cloudflare Worker",
|
||||
"description": "Honcho MCP Server",
|
||||
"main": "src/index.ts",
|
||||
"packageManager": "bun@1.2.0",
|
||||
"engines": {
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
"scripts": {
|
||||
"preinstall": "node -e \"const ua=process.env.npm_config_user_agent||'';if(ua.includes('npm')&&!ua.includes('bun')){console.error('❌ Please use bun instead of npm!\\n📦 Run: bun install\\n🌐 Install bun: https://bun.sh/');process.exit(1)}\"",
|
||||
"dev": "wrangler dev",
|
||||
"stdio": "bun src/stdio.ts",
|
||||
"http": "bun src/http.ts",
|
||||
"deploy": "wrangler deploy",
|
||||
"deploy:staging": "wrangler deploy --env staging"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Honcho } from "@honcho-ai/sdk";
|
|||
export interface HonchoConfig {
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
/** From X-Honcho-Workspace-ID when set. */
|
||||
/** From X-Honcho-Workspace-ID (HTTP) or HONCHO_WORKSPACE_ID (stdio). */
|
||||
workspaceId?: string;
|
||||
}
|
||||
|
||||
|
|
@ -12,6 +12,12 @@ export interface Env {
|
|||
ALERT_WEBHOOK_URL?: string;
|
||||
}
|
||||
|
||||
export interface EnvConfig {
|
||||
HONCHO_API_KEY?: string;
|
||||
HONCHO_API_URL?: string;
|
||||
HONCHO_WORKSPACE_ID?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse configuration from request headers and Worker env bindings.
|
||||
* Throws only when the Authorization bearer token is missing/empty.
|
||||
|
|
@ -48,8 +54,23 @@ export function parseConfig(request: Request, env: Env = {}): HonchoConfig {
|
|||
};
|
||||
}
|
||||
|
||||
/** Parse configuration from process env. */
|
||||
export function parseEnvConfig(env: EnvConfig): HonchoConfig {
|
||||
const apiKey = env.HONCHO_API_KEY?.trim();
|
||||
if (!apiKey) {
|
||||
throw new Error(
|
||||
"Missing HONCHO_API_KEY. Set HONCHO_API_KEY to your Honcho API key.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
apiKey,
|
||||
baseUrl: env.HONCHO_API_URL?.trim() || "https://api.honcho.dev",
|
||||
workspaceId: env.HONCHO_WORKSPACE_ID?.trim() || undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export const MISSING_WORKSPACE_ID_MESSAGE =
|
||||
"Missing workspace_id. Pass workspace_id on the next tool call, or set the X-Honcho-Workspace-ID header on the connection so it is used automatically.";
|
||||
"Missing workspace_id. Pass workspace_id on the next tool call, or set X-Honcho-Workspace-ID (HTTP) / HONCHO_WORKSPACE_ID (stdio).";
|
||||
|
||||
export function resolveWorkspaceId(
|
||||
config: HonchoConfig,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import { expect, test } from "bun:test";
|
||||
import { fetch } from "./http.ts";
|
||||
|
||||
const origin = "http://127.0.0.1:3000";
|
||||
|
||||
const initializeBody = {
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "initialize",
|
||||
params: {
|
||||
protocolVersion: "2024-11-05",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "test", version: "0.0.0" },
|
||||
},
|
||||
};
|
||||
|
||||
const pingBody = { jsonrpc: "2.0", id: 2, method: "ping" };
|
||||
|
||||
function mcpPost(headers: Record<string, string>, body: unknown) {
|
||||
return fetch(
|
||||
new Request(`${origin}/mcp`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json, text/event-stream",
|
||||
"Content-Type": "application/json",
|
||||
...headers,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
test("established sessions require the initialize bearer", async () => {
|
||||
const init = await mcpPost(
|
||||
{ Authorization: "Bearer key-a" },
|
||||
initializeBody,
|
||||
);
|
||||
expect(init.status).toBe(200);
|
||||
const sessionId = init.headers.get("mcp-session-id");
|
||||
expect(sessionId).toBeTruthy();
|
||||
|
||||
const missing = await mcpPost({ "mcp-session-id": sessionId! }, pingBody);
|
||||
expect(missing.status).toBe(401);
|
||||
|
||||
const wrong = await mcpPost(
|
||||
{ Authorization: "Bearer key-b", "mcp-session-id": sessionId! },
|
||||
pingBody,
|
||||
);
|
||||
expect(wrong.status).toBe(401);
|
||||
|
||||
const ok = await mcpPost(
|
||||
{ Authorization: "Bearer key-a", "mcp-session-id": sessionId! },
|
||||
pingBody,
|
||||
);
|
||||
expect(ok.status).toBe(200);
|
||||
});
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
||||
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
||||
import {
|
||||
createClientFactory,
|
||||
createUnscopedClient,
|
||||
parseConfig,
|
||||
type Env,
|
||||
type HonchoConfig,
|
||||
} from "./config.js";
|
||||
import { createServer } from "./server.js";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
|
||||
declare const process: {
|
||||
env: Record<string, string | undefined>;
|
||||
};
|
||||
|
||||
declare const Bun: {
|
||||
serve(options: {
|
||||
hostname: string;
|
||||
port: number;
|
||||
fetch(request: Request): Response | Promise<Response>;
|
||||
}): { hostname: string; port: number };
|
||||
};
|
||||
|
||||
const CORS_ORIGIN = "*";
|
||||
const CORS_METHODS = "GET, POST, DELETE, OPTIONS";
|
||||
const CORS_ALLOWED_HEADERS =
|
||||
"Content-Type, Authorization, X-Honcho-Workspace-ID, mcp-session-id, mcp-protocol-version, last-event-id";
|
||||
|
||||
const CORS_HEADERS: Record<string, string> = {
|
||||
"Access-Control-Allow-Origin": CORS_ORIGIN,
|
||||
"Access-Control-Allow-Methods": CORS_METHODS,
|
||||
"Access-Control-Allow-Headers": CORS_ALLOWED_HEADERS,
|
||||
"Access-Control-Expose-Headers": "WWW-Authenticate, mcp-session-id",
|
||||
};
|
||||
|
||||
const PROTECTED_RESOURCE_PATH = "/.well-known/oauth-protected-resource";
|
||||
const MCP_PATHS = new Set(["/", "/mcp"]);
|
||||
|
||||
type Session = {
|
||||
transport: WebStandardStreamableHTTPServerTransport;
|
||||
server: McpServer;
|
||||
lastSeen: number;
|
||||
apiKey: string;
|
||||
};
|
||||
|
||||
const sessions = new Map<string, Session>();
|
||||
const DEFAULT_SESSION_IDLE_MS = 30 * 60 * 1000;
|
||||
const DEFAULT_SESSION_MAX = 128;
|
||||
|
||||
function envInt(name: string, fallback: number): number {
|
||||
const n = Number(process.env[name]);
|
||||
return Number.isFinite(n) && n > 0 ? n : fallback;
|
||||
}
|
||||
|
||||
function dropSession(id: string): void {
|
||||
const session = sessions.get(id);
|
||||
if (!session) return;
|
||||
sessions.delete(id);
|
||||
void session.transport.close();
|
||||
void session.server.close();
|
||||
}
|
||||
|
||||
function sweepSessions(): void {
|
||||
const idleMs = envInt("MCP_SESSION_IDLE_MS", DEFAULT_SESSION_IDLE_MS);
|
||||
const now = Date.now();
|
||||
for (const [id, session] of sessions) {
|
||||
if (now - session.lastSeen > idleMs) dropSession(id);
|
||||
}
|
||||
}
|
||||
|
||||
function envBindings(): Env {
|
||||
return { HONCHO_API_URL: process.env.HONCHO_API_URL };
|
||||
}
|
||||
|
||||
function authorizationServer(): string {
|
||||
return process.env.HONCHO_API_URL?.trim() || "https://api.honcho.dev";
|
||||
}
|
||||
|
||||
function withCors(response: Response): Response {
|
||||
const headers = new Headers(response.headers);
|
||||
for (const [key, value] of Object.entries(CORS_HEADERS)) {
|
||||
headers.set(key, value);
|
||||
}
|
||||
return new Response(response.body, {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
headers,
|
||||
});
|
||||
}
|
||||
|
||||
function jsonResponse(
|
||||
body: unknown,
|
||||
status: number,
|
||||
extraHeaders?: Record<string, string>,
|
||||
): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
...CORS_HEADERS,
|
||||
...extraHeaders,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function configForRequest(request: Request) {
|
||||
return parseConfig(request, envBindings());
|
||||
}
|
||||
|
||||
function configOrUnauthorized(request: Request): HonchoConfig | Response {
|
||||
try {
|
||||
return configForRequest(request);
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : "Invalid request";
|
||||
return unauthorized(request, message);
|
||||
}
|
||||
}
|
||||
|
||||
function unauthorized(request: Request, message: string): Response {
|
||||
const resourceMetadata = `${new URL(request.url).origin}${PROTECTED_RESOURCE_PATH}`;
|
||||
return jsonResponse(
|
||||
{ error: message },
|
||||
401,
|
||||
{
|
||||
"WWW-Authenticate": `Bearer resource_metadata="${resourceMetadata}"`,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
async function handleMcp(request: Request): Promise<Response> {
|
||||
sweepSessions();
|
||||
const sessionId = request.headers.get("mcp-session-id");
|
||||
if (sessionId) {
|
||||
const existing = sessions.get(sessionId);
|
||||
if (existing) {
|
||||
const config = configOrUnauthorized(request);
|
||||
if (config instanceof Response) return config;
|
||||
if (config.apiKey !== existing.apiKey) {
|
||||
return unauthorized(
|
||||
request,
|
||||
"Authorization does not match this session.",
|
||||
);
|
||||
}
|
||||
existing.lastSeen = Date.now();
|
||||
return withCors(await existing.transport.handleRequest(request));
|
||||
}
|
||||
}
|
||||
|
||||
if (request.method !== "POST") {
|
||||
return jsonResponse(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Bad Request: No valid session ID provided",
|
||||
},
|
||||
id: null,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return jsonResponse(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
error: { code: -32700, message: "Parse error: Invalid JSON" },
|
||||
id: null,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const messages = Array.isArray(body) ? body : [body];
|
||||
if (!messages.some((message) => isInitializeRequest(message))) {
|
||||
return jsonResponse(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Bad Request: No valid session ID provided",
|
||||
},
|
||||
id: null,
|
||||
},
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const config = configOrUnauthorized(request);
|
||||
if (config instanceof Response) return config;
|
||||
|
||||
const server = createServer({
|
||||
config,
|
||||
clientFor: createClientFactory(config),
|
||||
unscoped: createUnscopedClient(config),
|
||||
});
|
||||
|
||||
const maxSessions = envInt("MCP_SESSION_MAX", DEFAULT_SESSION_MAX);
|
||||
if (sessions.size >= maxSessions) {
|
||||
return jsonResponse(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
error: {
|
||||
code: -32000,
|
||||
message: "Too many active sessions",
|
||||
},
|
||||
id: null,
|
||||
},
|
||||
503,
|
||||
);
|
||||
}
|
||||
|
||||
const transport = new WebStandardStreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => crypto.randomUUID(),
|
||||
onsessioninitialized: (id) => {
|
||||
sessions.set(id, {
|
||||
transport,
|
||||
server,
|
||||
lastSeen: Date.now(),
|
||||
apiKey: config.apiKey,
|
||||
});
|
||||
},
|
||||
});
|
||||
transport.onclose = () => {
|
||||
const id = transport.sessionId;
|
||||
if (id) sessions.delete(id);
|
||||
};
|
||||
|
||||
await server.connect(transport);
|
||||
return withCors(
|
||||
await transport.handleRequest(request, { parsedBody: body }),
|
||||
);
|
||||
}
|
||||
|
||||
export async function fetch(request: Request): Promise<Response> {
|
||||
if (request.method === "OPTIONS") {
|
||||
return new Response(null, { status: 204, headers: CORS_HEADERS });
|
||||
}
|
||||
|
||||
const pathname = new URL(request.url).pathname;
|
||||
|
||||
if (pathname === "/health") {
|
||||
return jsonResponse({ status: "ok" }, 200);
|
||||
}
|
||||
|
||||
if (pathname === PROTECTED_RESOURCE_PATH) {
|
||||
return jsonResponse(
|
||||
{
|
||||
resource: new URL(request.url).origin,
|
||||
authorization_servers: [authorizationServer()],
|
||||
bearer_methods_supported: ["header"],
|
||||
scopes_supported: ["read", "write"],
|
||||
},
|
||||
200,
|
||||
);
|
||||
}
|
||||
|
||||
if (!MCP_PATHS.has(pathname)) {
|
||||
return jsonResponse({ error: "Not Found" }, 404);
|
||||
}
|
||||
|
||||
try {
|
||||
return await handleMcp(request);
|
||||
} catch (e) {
|
||||
const message =
|
||||
e instanceof Error ? e.message : "Internal server error";
|
||||
return jsonResponse({ error: message }, 500);
|
||||
}
|
||||
}
|
||||
|
||||
const isMain = Boolean((import.meta as { main?: boolean }).main);
|
||||
if (isMain) {
|
||||
const hostname = process.env.HOST?.trim() || "0.0.0.0";
|
||||
const port = Number(process.env.PORT) || 3000;
|
||||
Bun.serve({ hostname, port, fetch });
|
||||
console.error(`honcho-mcp listening on http://${hostname}:${port}`);
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
createClientFactory,
|
||||
createUnscopedClient,
|
||||
parseEnvConfig,
|
||||
} from "./config.js";
|
||||
import { createServer } from "./server.js";
|
||||
|
||||
declare const process: {
|
||||
env: Record<string, string | undefined>;
|
||||
exit(code?: number): never;
|
||||
};
|
||||
|
||||
try {
|
||||
const config = parseEnvConfig({
|
||||
HONCHO_API_KEY: process.env.HONCHO_API_KEY,
|
||||
HONCHO_API_URL: process.env.HONCHO_API_URL,
|
||||
HONCHO_WORKSPACE_ID: process.env.HONCHO_WORKSPACE_ID,
|
||||
});
|
||||
const server = createServer({
|
||||
config,
|
||||
clientFor: createClientFactory(config),
|
||||
unscoped: createUnscopedClient(config),
|
||||
});
|
||||
await server.connect(new StdioServerTransport());
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : String(e);
|
||||
console.error(message);
|
||||
process.exit(1);
|
||||
}
|
||||
|
|
@ -10,5 +10,5 @@
|
|||
"types": ["@cloudflare/workers-types"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "src/**/*.test.ts"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue