Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions mcp-openclaw/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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`
Expand Down
1 change: 0 additions & 1 deletion mcp-openclaw/openclaw.plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
10 changes: 9 additions & 1 deletion mcp-openclaw/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down Expand Up @@ -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"
Expand Down
118 changes: 95 additions & 23 deletions mcp-openclaw/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,119 @@
/**
* @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
token_lifetime?: number
mcp_servers: Record<string, string>
}

export interface OpenClawPluginApi {
getConfig(): PluginConfig
registerTool(name: string, handler: (args: Record<string, unknown>) => Promise<unknown>): void
onShutdown(fn: () => Promise<void>): 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<string, unknown>
execute(toolCallId: string, params: Record<string, unknown>): Promise<unknown>
},
opts?: { optional?: boolean },
): void
registerService(service: {
id: string
start(): Promise<void> | void
stop(): Promise<void> | 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'
Expand Down
25 changes: 22 additions & 3 deletions mcp-openclaw/src/server-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
})
})

Expand Down Expand Up @@ -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,
},
])
})

Expand Down
44 changes: 34 additions & 10 deletions mcp-openclaw/src/server-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>
}

interface ManagedServer {
name: string
client: Client
transport: Transport
tools: Map<string, string> // prefixed name → original name
tools: Map<string, ManagedTool> // prefixed name → tool metadata
}

export interface ServerManagerOptions {
Expand Down Expand Up @@ -36,22 +42,40 @@ export class ServerManager {
await client.connect(transport)

const { tools } = await client.listTools()
const toolMap = new Map<string, string>()
const toolMap = new Map<string, ManagedTool>()
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<string, unknown> | 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<string, unknown>
}> {
const result: Array<{
prefixedName: string
serverName: string
originalName: string
description?: string
inputSchema?: Record<string, unknown>
}> = []
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,
})
}
}
Expand All @@ -63,9 +87,9 @@ export class ServerManager {
args: Record<string, unknown>,
): Promise<unknown> {
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}`)
Expand Down