mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-04-26 00:46:23 +02:00
server for rowboatx
This commit is contained in:
parent
ae877e70ae
commit
9ad6331fbc
38 changed files with 2223 additions and 1088 deletions
174
apps/cli/src/mcp/mcp.ts
Normal file
174
apps/cli/src/mcp/mcp.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
import container from "../di/container.js";
|
||||
import { Client } from "@modelcontextprotocol/sdk/client";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
||||
import z from "zod";
|
||||
import { IMcpConfigRepo } from "./repo.js";
|
||||
import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
||||
|
||||
export const StdioMcpServerConfig = z.object({
|
||||
type: z.literal("stdio").optional(),
|
||||
command: z.string(),
|
||||
args: z.array(z.string()).optional(),
|
||||
env: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
export const HttpMcpServerConfig = z.object({
|
||||
type: z.literal("http").optional(),
|
||||
url: z.string(),
|
||||
headers: z.record(z.string(), z.string()).optional(),
|
||||
});
|
||||
|
||||
export const McpServerDefinition = z.union([StdioMcpServerConfig, HttpMcpServerConfig]);
|
||||
|
||||
export const McpServerConfig = z.object({
|
||||
mcpServers: z.record(z.string(), McpServerDefinition),
|
||||
});
|
||||
|
||||
const connectionState = z.enum(["disconnected", "connected", "error"]);
|
||||
|
||||
export const McpServerList = z.object({
|
||||
mcpServers: z.record(z.string(), z.object({
|
||||
config: McpServerDefinition,
|
||||
state: connectionState,
|
||||
error: z.string().nullable(),
|
||||
})),
|
||||
});
|
||||
|
||||
/*
|
||||
inputSchema: {
|
||||
[x: string]: unknown;
|
||||
type: "object";
|
||||
properties?: Record<string, object> | undefined;
|
||||
required?: string[] | undefined;
|
||||
};
|
||||
*/
|
||||
export const Tool = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
inputSchema: z.object({
|
||||
type: z.literal("object"),
|
||||
properties: z.record(z.string(), z.any()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
}),
|
||||
outputSchema: z.object({
|
||||
type: z.literal("object"),
|
||||
properties: z.record(z.string(), z.any()).optional(),
|
||||
required: z.array(z.string()).optional(),
|
||||
}).optional(),
|
||||
})
|
||||
|
||||
export const ListToolsResponse = z.object({
|
||||
tools: z.array(Tool),
|
||||
nextCursor: z.string().optional(),
|
||||
});
|
||||
|
||||
type mcpState = {
|
||||
state: z.infer<typeof connectionState>,
|
||||
client: Client | null,
|
||||
error: string | null,
|
||||
};
|
||||
const clients: Record<string, mcpState> = {};
|
||||
|
||||
async function getClient(serverName: string): Promise<Client> {
|
||||
if (clients[serverName] && clients[serverName].state === "connected") {
|
||||
return clients[serverName].client!;
|
||||
}
|
||||
const repo = container.resolve<IMcpConfigRepo>('mcpConfigRepo');
|
||||
const { mcpServers } = await repo.getConfig();
|
||||
const config = mcpServers[serverName];
|
||||
if (!config) {
|
||||
throw new Error(`MCP server ${serverName} not found`);
|
||||
}
|
||||
let transport: Transport | undefined = undefined;
|
||||
try {
|
||||
// create transport
|
||||
if ("command" in config) {
|
||||
transport = new StdioClientTransport({
|
||||
command: config.command,
|
||||
args: config.args,
|
||||
env: config.env,
|
||||
});
|
||||
} else {
|
||||
try {
|
||||
transport = new StreamableHTTPClientTransport(new URL(config.url));
|
||||
} catch (error) {
|
||||
// if that fails, try sse transport
|
||||
transport = new SSEClientTransport(new URL(config.url));
|
||||
}
|
||||
}
|
||||
|
||||
if (!transport) {
|
||||
throw new Error(`No transport found for ${serverName}`);
|
||||
}
|
||||
|
||||
// create client
|
||||
const client = new Client({
|
||||
name: 'rowboatx',
|
||||
version: '1.0.0',
|
||||
});
|
||||
await client.connect(transport);
|
||||
|
||||
// store
|
||||
clients[serverName] = {
|
||||
state: "connected",
|
||||
client,
|
||||
error: null,
|
||||
};
|
||||
return client;
|
||||
} catch (error) {
|
||||
clients[serverName] = {
|
||||
state: "error",
|
||||
client: null,
|
||||
error: error instanceof Error ? error.message : "Unknown error",
|
||||
};
|
||||
transport?.close();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function cleanup() {
|
||||
for (const [serverName, { client }] of Object.entries(clients)) {
|
||||
await client?.transport?.close();
|
||||
await client?.close();
|
||||
delete clients[serverName];
|
||||
}
|
||||
}
|
||||
|
||||
export async function listServers(): Promise<z.infer<typeof McpServerList>> {
|
||||
const repo = container.resolve<IMcpConfigRepo>('mcpConfigRepo');
|
||||
const { mcpServers } = await repo.getConfig();
|
||||
const result: z.infer<typeof McpServerList> = {
|
||||
mcpServers: {},
|
||||
};
|
||||
for (const [serverName, config] of Object.entries(mcpServers)) {
|
||||
const state = clients[serverName];
|
||||
result.mcpServers[serverName] = {
|
||||
config,
|
||||
state: state ? state.state : "disconnected",
|
||||
error: state ? state.error : null,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function listTools(serverName: string, cursor?: string): Promise<z.infer<typeof ListToolsResponse>> {
|
||||
const client = await getClient(serverName);
|
||||
const { tools, nextCursor } = await client.listTools({
|
||||
cursor,
|
||||
});
|
||||
return {
|
||||
tools,
|
||||
nextCursor,
|
||||
}
|
||||
}
|
||||
|
||||
export async function executeTool(serverName: string, toolName: string, input: any): Promise<unknown> {
|
||||
const client = await getClient(serverName);
|
||||
const result = await client.callTool({
|
||||
name: toolName,
|
||||
arguments: input,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
45
apps/cli/src/mcp/repo.ts
Normal file
45
apps/cli/src/mcp/repo.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { WorkDir } from "../config/config.js";
|
||||
import { McpServerConfig } from "./mcp.js";
|
||||
import { McpServerDefinition } from "./mcp.js";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
import z from "zod";
|
||||
|
||||
export interface IMcpConfigRepo {
|
||||
getConfig(): Promise<z.infer<typeof McpServerConfig>>;
|
||||
upsert(serverName: string, config: z.infer<typeof McpServerDefinition>): Promise<void>;
|
||||
delete(serverName: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class FSMcpConfigRepo implements IMcpConfigRepo {
|
||||
private readonly configPath = path.join(WorkDir, "config", "mcp.json");
|
||||
|
||||
constructor() {
|
||||
this.ensureDefaultConfig();
|
||||
}
|
||||
|
||||
private async ensureDefaultConfig(): Promise<void> {
|
||||
try {
|
||||
await fs.access(this.configPath);
|
||||
} catch (error) {
|
||||
await fs.writeFile(this.configPath, JSON.stringify({ mcpServers: {} }, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
async getConfig(): Promise<z.infer<typeof McpServerConfig>> {
|
||||
const config = await fs.readFile(this.configPath, "utf8");
|
||||
return McpServerConfig.parse(JSON.parse(config));
|
||||
}
|
||||
|
||||
async upsert(serverName: string, config: z.infer<typeof McpServerDefinition>): Promise<void> {
|
||||
const conf = await this.getConfig();
|
||||
conf.mcpServers[serverName] = config;
|
||||
await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2));
|
||||
}
|
||||
|
||||
async delete(serverName: string): Promise<void> {
|
||||
const conf = await this.getConfig();
|
||||
delete conf.mcpServers[serverName];
|
||||
await fs.writeFile(this.configPath, JSON.stringify(conf, null, 2));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue