Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
41 changes: 38 additions & 3 deletions src/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
50 changes: 49 additions & 1 deletion test/tool-shaping.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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<string, unknown>

expect(shaped).toEqual({
servers: [
{
name: 'weather-server',
displayName: 'Weather Server',
transportType: 'stdio',
description: 'Weather tools',
},
],
})
})
})
82 changes: 82 additions & 0 deletions test/workflow-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }))
Expand Down
Loading