diff --git a/mcp-openclaw/README.md b/mcp-openclaw/README.md index 4010598..c1eefbc 100644 --- a/mcp-openclaw/README.md +++ b/mcp-openclaw/README.md @@ -18,11 +18,10 @@ Add to `~/.openclaw/openclaw.json`: { "plugins": { "entries": { - "aauth": { + "aauth-mcp": { "enabled": true, "config": { "agent_url": "https://user.github.io", - "delegate": "openclaw", "mcp_servers": { "my-files": "https://files-api.example.com/mcp", "my-db": "https://db-api.example.com/mcp" @@ -34,16 +33,20 @@ Add to `~/.openclaw/openclaw.json`: } ``` +The plugin id is `aauth-mcp` and must match the manifest id. + Tools from remote servers are registered with a prefix: `my-files_read_file`, `my-db_query`, etc. ## API -### `register(api, config)` +### `register(api)` -Plugin entry point called by OpenClaw. Connects to configured MCP servers and registers their tools. +Plugin entry point called by OpenClaw. Connects to configured MCP servers via +a `registerService` lifecycle and registers each remote tool as an OpenClaw +tool. The plugin reads its config from `api.pluginConfig`. ```ts -import { register } from '@aauth/mcp-openclaw' +import register from '@aauth/mcp-openclaw' ``` ### `ServerManager` diff --git a/mcp-openclaw/openclaw.plugin.json b/mcp-openclaw/openclaw.plugin.json index 772c22d..2298b5b 100644 --- a/mcp-openclaw/openclaw.plugin.json +++ b/mcp-openclaw/openclaw.plugin.json @@ -3,7 +3,6 @@ "name": "AAuth MCP", "description": "Connect to remote MCP servers with AAuth agent authentication", "version": "0.0.1", - "entry": "./dist/index.js", "configSchema": { "type": "object", "properties": { diff --git a/mcp-openclaw/package.json b/mcp-openclaw/package.json index 22c5549..9194f29 100644 --- a/mcp-openclaw/package.json +++ b/mcp-openclaw/package.json @@ -1,6 +1,6 @@ { "name": "@aauth/mcp-openclaw", - "version": "0.8.1", + "version": "0.8.2", "description": "OpenClaw plugin for AAuth-authenticated MCP server connections", "type": "module", "exports": { @@ -38,6 +38,14 @@ "@aauth/local-keys": "^0.8.0", "@modelcontextprotocol/sdk": "^1.15.1" }, + "openclaw": { + "extensions": [ + "./src/index.ts" + ], + "runtimeExtensions": [ + "./dist/index.js" + ] + }, "devDependencies": { "@types/node": "^20.0.0", "typescript": "^5.0.0" diff --git a/mcp-openclaw/src/index.ts b/mcp-openclaw/src/index.ts index 3297dec..5ca6e2f 100644 --- a/mcp-openclaw/src/index.ts +++ b/mcp-openclaw/src/index.ts @@ -1,6 +1,19 @@ +/** + * @aauth/mcp-openclaw — OpenClaw plugin for AAuth-authenticated MCP servers. + * + * Discovers tools on remote MCP servers reachable over HTTP and registers each + * as an OpenClaw tool. All HTTP traffic is signed with an AAuth agent token + * via `@aauth/mcp-agent`'s `createSignedFetch`. + */ + import { createAgentToken } from '@aauth/local-keys' import { ServerManager } from './server-manager.js' +export const id = 'aauth-mcp' +export const name = 'AAuth MCP' +export const description = + 'Connect to remote MCP servers with AAuth agent authentication' + export interface PluginConfig { agent_url?: string local?: string @@ -8,40 +21,99 @@ export interface PluginConfig { mcp_servers: Record } -export interface OpenClawPluginApi { - getConfig(): PluginConfig - registerTool(name: string, handler: (args: Record) => Promise): void - onShutdown(fn: () => Promise): void +/** + * Minimal slice of OpenClaw's plugin API surface used by this plugin. + * The real type is exported from `openclaw/plugin-sdk/plugin-entry`, but we + * inline the slice we need to keep the plugin dependency-free. + */ +interface OpenClawPluginApi { + pluginConfig?: PluginConfig + logger: { + info(msg: string, ...args: unknown[]): void + warn(msg: string, ...args: unknown[]): void + error(msg: string, ...args: unknown[]): void + } + registerTool( + tool: { + name: string + label?: string + description: string + parameters: Record + execute(toolCallId: string, params: Record): Promise + }, + opts?: { optional?: boolean }, + ): void + registerService(service: { + id: string + start(): Promise | void + stop(): Promise | void + }): void } -export const id = 'aauth-mcp' - -export function register(api: OpenClawPluginApi): void { - const config = api.getConfig() +export default function register(api: OpenClawPluginApi): void { + const config = (api.pluginConfig ?? { mcp_servers: {} }) as PluginConfig const { agent_url, local, token_lifetime, mcp_servers } = config - const getKeyMaterial = () => - createAgentToken({ - agentUrl: agent_url, - local: local ?? 'openclaw', - tokenLifetime: token_lifetime, - }) + if (!agent_url) { + api.logger.error( + `[${id}] missing required config "agent_url"; plugin will not connect to any MCP servers.`, + ) + return + } + + if (!mcp_servers || Object.keys(mcp_servers).length === 0) { + api.logger.warn( + `[${id}] no MCP servers configured (config.mcp_servers is empty); nothing to register.`, + ) + return + } const manager = new ServerManager({ servers: mcp_servers, - getKeyMaterial, + getKeyMaterial: () => + createAgentToken({ + agentUrl: agent_url, + local: local ?? 'openclaw', + tokenLifetime: token_lifetime, + }), }) - manager.connectAll().then(() => { - const tools = manager.getTools() - for (const tool of tools) { - api.registerTool(tool.prefixedName, (args) => - manager.callTool(tool.prefixedName, args), + api.registerService({ + id: `${id}/connection-manager`, + async start() { + try { + await manager.connectAll() + } catch (err) { + api.logger.error( + `[${id}] failed to connect to one or more MCP servers: ${(err as Error).message}`, + ) + return + } + + const tools = manager.getTools() + for (const tool of tools) { + api.registerTool({ + name: tool.prefixedName, + description: + tool.description ?? + `${tool.serverName}: ${tool.originalName} (AAuth MCP)`, + parameters: tool.inputSchema ?? { + type: 'object', + additionalProperties: true, + }, + execute: (_toolCallId, params) => + manager.callTool(tool.prefixedName, params).then((r) => r as unknown), + }) + } + + api.logger.info( + `[${id}] connected to ${Object.keys(mcp_servers).length} server(s); registered ${tools.length} tool(s).`, ) - } + }, + async stop() { + await manager.shutdown() + }, }) - - api.onShutdown(() => manager.shutdown()) } export { ServerManager } from './server-manager.js' diff --git a/mcp-openclaw/src/server-manager.test.ts b/mcp-openclaw/src/server-manager.test.ts index 44800ef..d6d1ca5 100644 --- a/mcp-openclaw/src/server-manager.test.ts +++ b/mcp-openclaw/src/server-manager.test.ts @@ -40,7 +40,14 @@ describe('ServerManager', () => { beforeEach(() => { vi.clearAllMocks() mockListTools.mockResolvedValue({ - tools: [{ name: 'read_file' }, { name: 'write_file' }], + tools: [ + { + name: 'read_file', + description: 'Read a file', + inputSchema: { type: 'object', properties: { path: { type: 'string' } } }, + }, + { name: 'write_file' }, + ], }) }) @@ -90,8 +97,20 @@ describe('ServerManager', () => { const tools = manager.getTools() expect(tools).toEqual([ - { prefixedName: 'myfiles_read_file', serverName: 'myfiles', originalName: 'read_file' }, - { prefixedName: 'myfiles_write_file', serverName: 'myfiles', originalName: 'write_file' }, + { + prefixedName: 'myfiles_read_file', + serverName: 'myfiles', + originalName: 'read_file', + description: 'Read a file', + inputSchema: { type: 'object', properties: { path: { type: 'string' } } }, + }, + { + prefixedName: 'myfiles_write_file', + serverName: 'myfiles', + originalName: 'write_file', + description: undefined, + inputSchema: undefined, + }, ]) }) diff --git a/mcp-openclaw/src/server-manager.ts b/mcp-openclaw/src/server-manager.ts index 54520f7..08a3437 100644 --- a/mcp-openclaw/src/server-manager.ts +++ b/mcp-openclaw/src/server-manager.ts @@ -4,11 +4,17 @@ import { createSignedFetch } from '@aauth/mcp-agent' import type { GetKeyMaterial } from '@aauth/mcp-agent' import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js' +interface ManagedTool { + originalName: string + description?: string + inputSchema?: Record +} + interface ManagedServer { name: string client: Client transport: Transport - tools: Map // prefixed name → original name + tools: Map // prefixed name → tool metadata } export interface ServerManagerOptions { @@ -36,22 +42,40 @@ export class ServerManager { await client.connect(transport) const { tools } = await client.listTools() - const toolMap = new Map() + const toolMap = new Map() for (const tool of tools) { - toolMap.set(`${name}_${tool.name}`, tool.name) + toolMap.set(`${name}_${tool.name}`, { + originalName: tool.name, + description: tool.description, + inputSchema: tool.inputSchema as Record | undefined, + }) } this.servers.set(name, { name, client, transport, tools: toolMap }) } - getTools(): Array<{ prefixedName: string; serverName: string; originalName: string; description?: string }> { - const result: Array<{ prefixedName: string; serverName: string; originalName: string; description?: string }> = [] + getTools(): Array<{ + prefixedName: string + serverName: string + originalName: string + description?: string + inputSchema?: Record + }> { + const result: Array<{ + prefixedName: string + serverName: string + originalName: string + description?: string + inputSchema?: Record + }> = [] for (const [, server] of this.servers) { - for (const [prefixedName, originalName] of server.tools) { + for (const [prefixedName, meta] of server.tools) { result.push({ prefixedName, serverName: server.name, - originalName, + originalName: meta.originalName, + description: meta.description, + inputSchema: meta.inputSchema, }) } } @@ -63,9 +87,9 @@ export class ServerManager { args: Record, ): Promise { for (const [, server] of this.servers) { - const originalName = server.tools.get(prefixedName) - if (originalName) { - return server.client.callTool({ name: originalName, arguments: args }) + const meta = server.tools.get(prefixedName) + if (meta) { + return server.client.callTool({ name: meta.originalName, arguments: args }) } } throw new Error(`Unknown tool: ${prefixedName}`)