diff --git a/README.md b/README.md index 7da9180..3d6fcd6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This server exposes MissionSquad account-scoped operations for models, agents, p - Strict TypeScript and Zod-validated tool inputs - Multipart file upload support (`POST /v1/files`) - Bounded binary file-content retrieval (`GET /v1/files/:id/content`) with truncation metadata +- Compact MCP server discovery output for installed/enabled servers only - Build/test CI and npm publish workflow ## Verified API Coverage @@ -186,6 +187,14 @@ Returns a compact summary with: This tool is intended for discovery and agent/tool selection workflows. +### `msq_list_servers` + +Returns a compact server list for discovery workflows: + +- `servers`: array of records with only `name`, `displayName`, `transportType`, and `description` + +This tool filters out servers that are not installed or not enabled. + ### `msq_list_server_tools` Returns the raw tool inventory for a single MCP server: diff --git a/package.json b/package.json index 7d1ea3f..e058793 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@missionsquad/mcp-msq", - "version": "0.3.3", + "version": "0.3.4", "description": "MCP server interface for the MissionSquad API", "type": "module", "main": "dist/index.js", diff --git a/src/tools.ts b/src/tools.ts index b07d667..66239b7 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -343,6 +343,39 @@ export function summarizeToolInventories(payload: unknown): unknown { } } +type ServerInventorySummary = { + name: string + displayName: string + transportType: string + description: string +} + +export function summarizeServerInventories(payload: unknown): unknown { + if (!isRecord(payload)) { + return payload + } + + const rawServers = Array.isArray(payload.servers) ? payload.servers : [] + + const servers: ServerInventorySummary[] = rawServers + .filter(isRecord) + .filter((server) => server.installed === true && server.enabled === true) + .map((server) => ({ + name: typeof server.name === 'string' ? server.name : '', + displayName: + typeof server.displayName === 'string' && server.displayName.trim().length > 0 + ? server.displayName + : typeof server.name === 'string' + ? server.name + : '', + transportType: typeof server.transportType === 'string' ? server.transportType : 'unknown', + description: typeof server.description === 'string' ? server.description : '', + })) + .filter((server) => server.name.length > 0) + + return { servers } +} + const msqTools = [ defineTool({ name: 'msq_list_models', @@ -687,13 +720,15 @@ const msqTools = [ }), defineTool({ name: 'msq_list_servers', - description: 'List MissionSquad MCP server inventory and status.', + description: + 'List installed and enabled MissionSquad MCP servers in a compact discovery-friendly shape. ' + + 'Each result includes only `name`, `displayName`, `transportType`, and `description`.', parameters: EmptySchema, run: async (client) => - client.requestJson({ + summarizeServerInventories(await client.requestJson({ method: 'GET', path: 'core/servers', - }), + })), }), defineTool({ name: 'msq_list_server_tools', diff --git a/test/tool-shaping.test.ts b/test/tool-shaping.test.ts index 0985ec8..306952e 100644 --- a/test/tool-shaping.test.ts +++ b/test/tool-shaping.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest' -import { summarizeCoreConfig, summarizeToolInventories } from '../src/tools.js' +import { summarizeCoreConfig, summarizeServerInventories, summarizeToolInventories } from '../src/tools.js' describe('MissionSquad MCP tool shaping', () => { it('summarizes core config maps into compact list-friendly arrays', () => { @@ -64,4 +64,52 @@ describe('MissionSquad MCP tool shaping', () => { expect(shaped.serverNames).toEqual(['webtools', 'missionsquad']) expect(shaped.counts).toEqual({ servers: 2, tools: 3 }) }) + + it('summarizes server inventories into a compact filtered list', () => { + const input = { + success: true, + servers: [ + { + name: 'weather-server', + displayName: 'Weather Server', + transportType: 'stdio', + description: 'Weather tools', + installed: true, + enabled: true, + command: 'node', + args: ['server.js'], + env: { NODE_ENV: 'production' }, + }, + { + name: 'disabled-server', + displayName: 'Disabled Server', + transportType: 'streamable_http', + description: 'Should be omitted', + installed: true, + enabled: false, + }, + { + name: 'uninstalled-server', + displayName: 'Uninstalled Server', + transportType: 'streamable_http', + description: 'Should be omitted', + installed: false, + enabled: true, + }, + ], + } + + const shaped = summarizeServerInventories(input) as Record + + expect(shaped).toEqual({ + servers: [ + { + name: 'weather-server', + displayName: 'Weather Server', + transportType: 'stdio', + description: 'Weather tools', + }, + ], + }) + }) }) diff --git a/test/workflow-tools.test.ts b/test/workflow-tools.test.ts index 9561cce..94fe3b6 100644 --- a/test/workflow-tools.test.ts +++ b/test/workflow-tools.test.ts @@ -206,6 +206,88 @@ describe('MissionSquad workflow tools', () => { expect(result).toEqual({ workflows }) }) + it('lists installed and enabled servers in a compact shape', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ + success: true, + servers: [ + { + name: 'weather-server', + displayName: 'Weather Server', + transportType: 'stdio', + description: 'Weather tools', + installed: true, + enabled: true, + command: 'node', + args: ['server.js'], + env: { NODE_ENV: 'production' }, + }, + { + name: 'disabled-server', + displayName: 'Disabled Server', + transportType: 'streamable_http', + description: 'Should be omitted', + installed: true, + enabled: false, + }, + ], + })) + + const result = await callTool('msq_list_servers', {}) + const { url, init } = getRequest(fetchMock) + + expect(url.pathname).toBe('/v1/core/servers') + expect(init.method).toBe('GET') + expect(result).toEqual({ + servers: [ + { + name: 'weather-server', + displayName: 'Weather Server', + transportType: 'stdio', + description: 'Weather tools', + }, + ], + }) + }) + + it('lists tools for a single server using the per-server MCP route', async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ + success: true, + tools: [ + { + name: 'weather', + description: 'Get weather information', + inputSchema: { + type: 'object', + properties: { + location: { type: 'string' }, + }, + }, + }, + ], + })) + + const result = await callTool('msq_list_server_tools', { serverName: 'weather-server' }) + const { url, init } = getRequest(fetchMock) + + expect(url.pathname).toBe('/v1/mcp/servers/weather-server/tools') + expect(init.method).toBe('GET') + expect(result).toEqual({ + success: true, + tools: [ + { + name: 'weather', + description: 'Get weather information', + inputSchema: { + type: 'object', + properties: { + location: { type: 'string' }, + }, + }, + }, + ], + }) + }) + it('gets a workflow by exact id match and errors when missing', async () => { const workflows = [buildWorkflowConfig('wf-1'), buildWorkflowConfig('wf-2')] fetchMock.mockResolvedValueOnce(jsonResponse({ success: true, data: workflows }))