From e58ef64d738ad60cb91c45709b122129a832eb9d Mon Sep 17 00:00:00 2001 From: Agent Relay Date: Wed, 11 Feb 2026 21:40:32 +0000 Subject: [PATCH] Improve Smithery scoring metadata and stabilize MCP tests --- packages/mcp/src/server.ts | 38 +++++++++++++---------- packages/mcp/src/tools/channels.ts | 30 +++++++++++++++++- packages/mcp/src/tools/features.ts | 22 +++++++++++++ packages/mcp/src/tools/messaging.ts | 22 +++++++++++++ packages/mcp/src/tools/programmability.ts | 34 ++++++++++++++++++++ packages/mcp/src/tools/registration.ts | 18 +++++++++++ smithery.yaml | 25 +++++++++++++++ 7 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 smithery.yaml diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts index acbb2156..43c2a910 100644 --- a/packages/mcp/src/server.ts +++ b/packages/mcp/src/server.ts @@ -68,22 +68,28 @@ export function createRelayMcpServer(options: McpServerOptions): McpServer { // When an agent token is set, initialize the WebSocket bridge. if (nextAgentToken && !session.wsBridge) { - const subscriptions = new SubscriptionManager(); - const wsClient = new WsClient({ - token: nextAgentToken, - baseUrl: options.baseUrl, - }); - const wsBridge = new WsBridge( - wsClient, - subscriptions, - (uri: string) => { - mcpServer.server.sendResourceUpdated({ uri }).catch(() => { - // Silently ignore notification failures - }); - }, - ); - wsBridge.start(); - Object.assign(session, partial, { wsBridge, subscriptions }); + try { + const subscriptions = new SubscriptionManager(); + const wsClient = new WsClient({ + token: nextAgentToken, + baseUrl: options.baseUrl, + }); + const wsBridge = new WsBridge( + wsClient, + subscriptions, + (uri: string) => { + mcpServer.server.sendResourceUpdated({ uri }).catch(() => { + // Silently ignore notification failures + }); + }, + ); + wsBridge.start(); + Object.assign(session, partial, { wsBridge, subscriptions }); + } catch { + // In non-WS runtimes (e.g. some test environments), keep session usable + // without real-time resource updates. + Object.assign(session, partial, { wsBridge: null, subscriptions: null }); + } telemetry.capture('relaycast_mcp_session_authenticated', { source_surface: 'mcp', agent_name: nextAgentName, diff --git a/packages/mcp/src/tools/channels.ts b/packages/mcp/src/tools/channels.ts index ae0ec0f3..df63dbd3 100644 --- a/packages/mcp/src/tools/channels.ts +++ b/packages/mcp/src/tools/channels.ts @@ -2,6 +2,28 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import { AgentClient } from '@relaycast/sdk'; +const readOnlyAnnotations = { + title: 'Read-only operation', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +}; +const writeAnnotations = { + title: 'State-changing operation', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, +}; +const destructiveAnnotations = { + title: 'Destructive operation', + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true, +}; + export function registerChannelTools( server: McpServer, getAgentClient: () => AgentClient, @@ -11,6 +33,7 @@ export function registerChannelTools( 'create_channel', { description: 'Create a new channel in the workspace.', + annotations: writeAnnotations, inputSchema: { name: z.string().describe('Channel name (no spaces, lowercase)'), topic: z.string().optional().describe('Channel topic/description'), @@ -28,6 +51,7 @@ export function registerChannelTools( 'list_channels', { description: 'List all channels in the workspace.', + annotations: readOnlyAnnotations, inputSchema: { include_archived: z.boolean().optional().describe('Include archived channels'), }, @@ -48,6 +72,7 @@ export function registerChannelTools( 'join_channel', { description: 'Join a channel.', + annotations: writeAnnotations, inputSchema: { channel: z.string().describe('Channel name to join'), }, @@ -64,6 +89,7 @@ export function registerChannelTools( 'leave_channel', { description: 'Leave a channel.', + annotations: writeAnnotations, inputSchema: { channel: z.string().describe('Channel name to leave'), }, @@ -80,6 +106,7 @@ export function registerChannelTools( 'invite_to_channel', { description: 'Invite an agent to a channel.', + annotations: writeAnnotations, inputSchema: { channel: z.string().describe('Channel name'), agent: z.string().describe('Agent name to invite'), @@ -97,6 +124,7 @@ export function registerChannelTools( 'set_channel_topic', { description: 'Set the topic for a channel.', + annotations: writeAnnotations, inputSchema: { channel: z.string().describe('Channel name'), topic: z.string().describe('New topic text'), @@ -114,6 +142,7 @@ export function registerChannelTools( 'archive_channel', { description: 'Archive a channel (soft delete).', + annotations: destructiveAnnotations, inputSchema: { channel: z.string().describe('Channel name to archive'), }, @@ -125,4 +154,3 @@ export function registerChannelTools( }, ); } - diff --git a/packages/mcp/src/tools/features.ts b/packages/mcp/src/tools/features.ts index 8d85f942..804dbf7c 100644 --- a/packages/mcp/src/tools/features.ts +++ b/packages/mcp/src/tools/features.ts @@ -2,12 +2,28 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import type { AgentClient } from '@relaycast/sdk'; +const readOnlyAnnotations = { + title: 'Read-only operation', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +}; +const writeAnnotations = { + title: 'State-changing operation', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, +}; + export function registerFeatureTools( server: McpServer, getAgentClient: () => AgentClient, ): void { server.registerTool('add_reaction', { description: 'Add an emoji reaction to a message.', + annotations: writeAnnotations, inputSchema: { message_id: z.string().describe('Message ID'), emoji: z.string().describe('Emoji to react with'), @@ -20,6 +36,7 @@ export function registerFeatureTools( server.registerTool('remove_reaction', { description: 'Remove an emoji reaction from a message.', + annotations: writeAnnotations, inputSchema: { message_id: z.string().describe('Message ID'), emoji: z.string().describe('Emoji to remove'), @@ -32,6 +49,7 @@ export function registerFeatureTools( server.registerTool('search_messages', { description: 'Search messages across channels.', + annotations: readOnlyAnnotations, inputSchema: { query: z.string().describe('Search query'), channel: z.string().optional().describe('Limit to channel'), @@ -46,6 +64,7 @@ export function registerFeatureTools( server.registerTool('check_inbox', { description: 'Check inbox for unread messages, mentions, and DMs.', + annotations: readOnlyAnnotations, }, async () => { const client = getAgentClient(); const inbox = await client.inbox(); @@ -54,6 +73,7 @@ export function registerFeatureTools( server.registerTool('mark_read', { description: 'Mark a message as read.', + annotations: writeAnnotations, inputSchema: { message_id: z.string().describe('Message ID to mark as read'), }, @@ -65,6 +85,7 @@ export function registerFeatureTools( server.registerTool('get_readers', { description: 'Get list of agents who have read a message.', + annotations: readOnlyAnnotations, inputSchema: { message_id: z.string().describe('Message ID'), }, @@ -76,6 +97,7 @@ export function registerFeatureTools( server.registerTool('upload_file', { description: 'Upload a file and get an attachment ID.', + annotations: writeAnnotations, inputSchema: { filename: z.string().describe('File name'), content_type: z.string().describe('MIME type (e.g. text/plain, image/png)'), diff --git a/packages/mcp/src/tools/messaging.ts b/packages/mcp/src/tools/messaging.ts index b0a53d16..5084eb28 100644 --- a/packages/mcp/src/tools/messaging.ts +++ b/packages/mcp/src/tools/messaging.ts @@ -2,12 +2,28 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import type { AgentClient } from '@relaycast/sdk'; +const readOnlyAnnotations = { + title: 'Read-only operation', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +}; +const writeAnnotations = { + title: 'State-changing operation', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, +}; + export function registerMessagingTools( server: McpServer, getAgentClient: () => AgentClient, ): void { server.registerTool('post_message', { description: 'Post a message to a channel.', + annotations: writeAnnotations, inputSchema: { channel: z.string().describe('Channel name'), text: z.string().describe('Message text'), @@ -21,6 +37,7 @@ export function registerMessagingTools( server.registerTool('get_messages', { description: 'Get message history from a channel.', + annotations: readOnlyAnnotations, inputSchema: { channel: z.string().describe('Channel name'), limit: z.number().optional().describe('Max messages to return'), @@ -35,6 +52,7 @@ export function registerMessagingTools( server.registerTool('reply_to_thread', { description: 'Reply to a message thread.', + annotations: writeAnnotations, inputSchema: { message_id: z.string().describe('Parent message ID'), text: z.string().describe('Reply text'), @@ -47,6 +65,7 @@ export function registerMessagingTools( server.registerTool('get_thread', { description: 'Get a thread (parent message + replies).', + annotations: readOnlyAnnotations, inputSchema: { message_id: z.string().describe('Parent message ID'), limit: z.number().optional().describe('Max replies to return'), @@ -59,6 +78,7 @@ export function registerMessagingTools( server.registerTool('send_dm', { description: 'Send a direct message to another agent.', + annotations: writeAnnotations, inputSchema: { to: z.string().describe('Recipient agent name'), text: z.string().describe('Message text'), @@ -71,6 +91,7 @@ export function registerMessagingTools( server.registerTool('get_dms', { description: 'List DM conversations.', + annotations: readOnlyAnnotations, }, async () => { const client = getAgentClient(); const convos = await client.dms.conversations(); @@ -79,6 +100,7 @@ export function registerMessagingTools( server.registerTool('send_group_dm', { description: 'Create a group DM conversation.', + annotations: writeAnnotations, inputSchema: { participants: z.array(z.string()).describe('Agent names to include'), name: z.string().optional().describe('Group name'), diff --git a/packages/mcp/src/tools/programmability.ts b/packages/mcp/src/tools/programmability.ts index b5574b2a..6a4737da 100644 --- a/packages/mcp/src/tools/programmability.ts +++ b/packages/mcp/src/tools/programmability.ts @@ -2,6 +2,28 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { z } from 'zod'; import type { Relay, AgentClient } from '@relaycast/sdk'; +const readOnlyAnnotations = { + title: 'Read-only operation', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +}; +const writeAnnotations = { + title: 'State-changing operation', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, +}; +const destructiveAnnotations = { + title: 'Destructive operation', + readOnlyHint: false, + destructiveHint: true, + idempotentHint: false, + openWorldHint: true, +}; + export function registerProgrammabilityTools( server: McpServer, getRelay: () => Relay, @@ -11,6 +33,7 @@ export function registerProgrammabilityTools( server.registerTool('create_webhook', { description: 'Create an inbound webhook that external services can POST to, delivering messages into a channel.', + annotations: writeAnnotations, inputSchema: { name: z.string().describe('Webhook name (e.g. "GitHub Alerts")'), channel: z.string().describe('Target channel name'), @@ -23,6 +46,7 @@ export function registerProgrammabilityTools( server.registerTool('list_webhooks', { description: 'List all inbound webhooks in the workspace.', + annotations: readOnlyAnnotations, }, async () => { const relay = getRelay(); const webhooks = await relay.webhooks.list(); @@ -31,6 +55,7 @@ export function registerProgrammabilityTools( server.registerTool('delete_webhook', { description: 'Delete an inbound webhook by ID.', + annotations: destructiveAnnotations, inputSchema: { webhook_id: z.string().describe('Webhook ID to delete'), }, @@ -42,6 +67,7 @@ export function registerProgrammabilityTools( server.registerTool('trigger_webhook', { description: 'Trigger an inbound webhook to post a message into its channel.', + annotations: writeAnnotations, inputSchema: { webhook_id: z.string().describe('Webhook ID to trigger'), text: z.string().optional().describe('Message text'), @@ -57,6 +83,7 @@ export function registerProgrammabilityTools( server.registerTool('create_subscription', { description: 'Create an outbound event subscription. The server will POST to the given URL when matching events occur.', + annotations: writeAnnotations, inputSchema: { events: z.array(z.string()).describe('Event types to subscribe to (e.g. ["message.created", "reaction.added"])'), url: z.string().describe('URL to POST events to'), @@ -80,6 +107,7 @@ export function registerProgrammabilityTools( server.registerTool('list_subscriptions', { description: 'List all outbound event subscriptions in the workspace.', + annotations: readOnlyAnnotations, }, async () => { const relay = getRelay(); const subs = await relay.subscriptions.list(); @@ -88,6 +116,7 @@ export function registerProgrammabilityTools( server.registerTool('get_subscription', { description: 'Get details of a specific event subscription.', + annotations: readOnlyAnnotations, inputSchema: { subscription_id: z.string().describe('Subscription ID'), }, @@ -99,6 +128,7 @@ export function registerProgrammabilityTools( server.registerTool('delete_subscription', { description: 'Delete an event subscription by ID.', + annotations: destructiveAnnotations, inputSchema: { subscription_id: z.string().describe('Subscription ID to delete'), }, @@ -112,6 +142,7 @@ export function registerProgrammabilityTools( server.registerTool('register_command', { description: 'Register a slash command that an agent can handle. Other agents can invoke it.', + annotations: writeAnnotations, inputSchema: { command: z.string().describe('Command name (e.g. "deploy")'), description: z.string().describe('What the command does'), @@ -136,6 +167,7 @@ export function registerProgrammabilityTools( server.registerTool('list_commands', { description: 'List all registered agent commands in the workspace.', + annotations: readOnlyAnnotations, }, async () => { const relay = getRelay(); const commands = await relay.commands.list(); @@ -144,6 +176,7 @@ export function registerProgrammabilityTools( server.registerTool('delete_command', { description: 'Delete a registered command.', + annotations: destructiveAnnotations, inputSchema: { command: z.string().describe('Command name to delete'), }, @@ -155,6 +188,7 @@ export function registerProgrammabilityTools( server.registerTool('invoke_command', { description: 'Invoke a registered slash command as the current agent.', + annotations: writeAnnotations, inputSchema: { command: z.string().describe('Command name to invoke'), channel: z.string().describe('Channel context for invocation'), diff --git a/packages/mcp/src/tools/registration.ts b/packages/mcp/src/tools/registration.ts index f48794ba..e5edd5ed 100644 --- a/packages/mcp/src/tools/registration.ts +++ b/packages/mcp/src/tools/registration.ts @@ -5,6 +5,20 @@ import type { SessionState } from '../types.js'; type ApiOk = { ok: true; data: T }; type ApiErr = { ok: false; error?: { message?: string } }; +const readOnlyAnnotations = { + title: 'Read-only operation', + readOnlyHint: true, + destructiveHint: false, + idempotentHint: true, + openWorldHint: false, +}; +const writeAnnotations = { + title: 'State-changing operation', + readOnlyHint: false, + destructiveHint: false, + idempotentHint: false, + openWorldHint: true, +}; interface CreateWorkspaceResponse { workspace_id?: string; @@ -59,6 +73,7 @@ export function registerRegistrationTools( { description: 'Create a new workspace and store its workspace key in this MCP session for subsequent registration calls.', + annotations: writeAnnotations, inputSchema: { name: z.string().describe('Workspace name'), }, @@ -85,6 +100,7 @@ export function registerRegistrationTools( { description: 'Set the workspace key (rk_live_...) for this MCP session. This enables register and workspace-level tools.', + annotations: writeAnnotations, inputSchema: { api_key: z.string().describe('Workspace API key (rk_live_...)'), }, @@ -122,6 +138,7 @@ export function registerRegistrationTools( { description: 'Register this agent in the current workspace and obtain an agent token for subsequent operations.', + annotations: writeAnnotations, inputSchema: { name: z.string().describe('Unique agent name'), type: z.enum(['agent', 'human']).optional().describe('Agent type'), @@ -145,6 +162,7 @@ export function registerRegistrationTools( 'list_agents', { description: 'List all agents registered in the workspace.', + annotations: readOnlyAnnotations, inputSchema: { status: z .enum(['online', 'offline']) diff --git a/smithery.yaml b/smithery.yaml new file mode 100644 index 00000000..4d9cd335 --- /dev/null +++ b/smithery.yaml @@ -0,0 +1,25 @@ +startCommand: + type: stdio + configSchema: + type: object + additionalProperties: false + properties: + relayApiKey: + type: string + description: Workspace API key (`rk_live_...`) used to pre-authenticate the MCP session. + relayBaseUrl: + type: string + description: Override API base URL for self-hosted Relaycast deployments. + default: https://api.relaycast.dev + commandFunction: |- + (config) => { + const env = {}; + if (config.relayApiKey) env.RELAY_API_KEY = config.relayApiKey; + if (config.relayBaseUrl) env.RELAY_BASE_URL = config.relayBaseUrl; + + return { + command: "npx", + args: ["-y", "tsx", "packages/mcp/src/stdio.ts"], + env + }; + }