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
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-api",
"version": "1.11.6",
"version": "1.11.7",
"description": "MCP Servers exposed via HTTP API",
"main": "dist/index.js",
"repository": "missionsquad/mcp-api",
Expand Down
44 changes: 37 additions & 7 deletions src/services/packages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MCPService, MCPServer, MCPTransportType, assertTransportConfigCompatible } from './mcp'
import { MCPService, MCPServer, MCPTransportType, assertTransportConfigCompatible, type AddServerInput } from './mcp'
import { IndexDefinition, MongoConnectionParams, MongoDBClient } from '../utils/mongodb'
import { log, compareVersions } from '../utils/general'
import { env } from '../env'
Expand Down Expand Up @@ -36,7 +36,22 @@ export interface PackageInfo {
pipDependencies?: string[]
}

export interface InstallPackageRequest {
type PackageInstallServerMetadata = Pick<
AddServerInput,
| 'displayName'
| 'description'
| 'secretName'
| 'secretNames'
| 'secretFields'
| 'homepageUrl'
| 'repositoryUrl'
| 'licenseName'
| 'catalogProvider'
| 'catalogId'
| 'startupTimeout'
>

export interface InstallPackageRequest extends PackageInstallServerMetadata {
name: string
version?: string
serverName: string
Expand All @@ -47,7 +62,6 @@ export interface InstallPackageRequest {
url?: string
headers?: Record<string, string>
reconnectionOptions?: StreamableHTTPReconnectionOptions
secretName?: string
enabled?: boolean
failOnWarning?: boolean
runtime?: PackageRuntime
Expand Down Expand Up @@ -119,6 +133,22 @@ export class PackageService {
return path.join(venvAbsolutePath, binDir, exeName)
}

private buildServerMetadataInput(request: InstallPackageRequest): PackageInstallServerMetadata {
return {
displayName: request.displayName,
description: request.description,
secretName: request.secretName,
secretNames: request.secretNames,
secretFields: request.secretFields,
homepageUrl: request.homepageUrl,
repositoryUrl: request.repositoryUrl,
licenseName: request.licenseName,
catalogProvider: request.catalogProvider,
catalogId: request.catalogId,
startupTimeout: request.startupTimeout
}
Comment on lines +136 to +149
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

installPackage now forwards user-provided secretNames/secretFields (and other metadata) into MCPService.addServer, but for platform servers addServer doesn't apply the external-server validations (max secret fields, field name pattern, etc.). Since this input comes from the install request, it should be validated/normalized in PackageService (e.g., cap the number of secret fields and enforce the same field.name pattern used in MCPService.addServer) before persisting it on the server record.

Copilot uses AI. Check for mistakes.
}

private async ensureVenv(pythonExecutable: string, venvAbsolutePath: string): Promise<void> {
const pythonPath = this.venvPythonPath(venvAbsolutePath)
if (existsSync(pythonPath)) {
Expand Down Expand Up @@ -298,12 +328,12 @@ export class PackageService {
url,
headers,
reconnectionOptions,
secretName,
enabled = true,
failOnWarning = false
} = request
const resolvedTransportType: MCPTransportType = transportType ?? 'stdio'
const runtime: PackageRuntime = request.runtime ?? 'node'
const serverMetadata = this.buildServerMetadataInput(request)

if (runtime === 'python') {
if (!request.pythonModule) {
Expand Down Expand Up @@ -364,7 +394,7 @@ export class PackageService {
command: pythonCommand,
args: pythonArgs,
env: pythonEnv,
secretName,
...serverMetadata,
enabled
})

Expand Down Expand Up @@ -483,7 +513,7 @@ export class PackageService {
url: url!,
headers,
reconnectionOptions,
secretName,
...serverMetadata,
enabled
})
} else {
Expand Down Expand Up @@ -546,7 +576,7 @@ export class PackageService {
command: finalCommand,
args: stdioArgs,
env: stdioEnv,
secretName,
...serverMetadata,
enabled
})
}
Expand Down
157 changes: 156 additions & 1 deletion test/packages-python.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,25 @@ describe('PackageService python runtime support', () => {
runtime: 'python',
pythonModule: 'my_mcp_server',
pythonArgs: ['--port', '0'],
env: { PATH: '/usr/bin', CUSTOM_ENV: '1' }
env: { PATH: '/usr/bin', CUSTOM_ENV: '1' },
displayName: 'Python Server',
description: 'Python MCP test server',
secretNames: ['apiKey'],
secretFields: [
{
name: 'apiKey',
label: 'API key',
description: 'API key used by the package.',
required: true,
inputType: 'password'
}
],
homepageUrl: 'https://example.com/python-server',
repositoryUrl: 'https://github.com/example/python-server',
licenseName: 'MIT',
catalogProvider: 'manual',
catalogId: 'python-server',
startupTimeout: 600000
})

expect(result.success).toBe(true)
Expand All @@ -173,6 +191,16 @@ describe('PackageService python runtime support', () => {
command: string
args: string[]
env: Record<string, string>
displayName: string
description: string
secretNames: string[]
secretFields: Array<{ name: string; label: string; description: string; required: boolean; inputType: string }>
homepageUrl: string
repositoryUrl: string
licenseName: string
catalogProvider: string
catalogId: string
startupTimeout: number
}
const expectedVenvAbsolutePath = path.resolve(process.cwd(), path.join('packages/python', 'python-server'))
const expectedPythonCommand = path.join(
Expand All @@ -190,6 +218,133 @@ describe('PackageService python runtime support', () => {
path.join(expectedVenvAbsolutePath, process.platform === 'win32' ? 'Scripts' : 'bin')
)
).toBe(true)
expect(addServerInput.displayName).toBe('Python Server')
expect(addServerInput.description).toBe('Python MCP test server')
expect(addServerInput.secretNames).toEqual(['apiKey'])
expect(addServerInput.secretFields).toEqual([
{
name: 'apiKey',
label: 'API key',
description: 'API key used by the package.',
required: true,
inputType: 'password'
}
])
expect(addServerInput.homepageUrl).toBe('https://example.com/python-server')
expect(addServerInput.repositoryUrl).toBe('https://github.com/example/python-server')
expect(addServerInput.licenseName).toBe('MIT')
expect(addServerInput.catalogProvider).toBe('manual')
expect(addServerInput.catalogId).toBe('python-server')
expect(addServerInput.startupTimeout).toBe(600000)
})

test('installPackage forwards server metadata for node stdio packages', async () => {
const { service, mcpMock } = createService()

mcpMock.addServer.mockResolvedValue({
name: 'node-server',
transportType: 'stdio',
Comment on lines +241 to +246
Copy link

Copilot AI Apr 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This spec file/describe block is scoped to “python runtime support”, but these newly added cases also cover node stdio and streamable HTTP installs. Consider moving these new tests into a more appropriately named suite/file (or splitting describe blocks) so future changes to node/HTTP install behavior are discoverable.

Copilot uses AI. Check for mistakes.
command: 'node',
args: [],
env: {},
status: 'disconnected',
enabled: true
})

const result = await service.installPackage({
name: '@example/node-mcp',
serverName: 'node-server',
command: 'node',
args: ['./server.js'],
secretNames: ['token'],
secretFields: [
{
name: 'token',
label: 'Token',
description: 'Token used by the package.',
required: true,
inputType: 'password'
}
],
homepageUrl: 'https://example.com/node-mcp',
repositoryUrl: 'https://github.com/example/node-mcp',
startupTimeout: 300000
})

expect(result.success).toBe(true)
expect(mcpMock.addServer).toHaveBeenCalledWith(
expect.objectContaining({
name: 'node-server',
transportType: 'stdio',
command: 'node',
args: ['./server.js'],
secretNames: ['token'],
secretFields: [
{
name: 'token',
label: 'Token',
description: 'Token used by the package.',
required: true,
inputType: 'password'
}
],
homepageUrl: 'https://example.com/node-mcp',
repositoryUrl: 'https://github.com/example/node-mcp',
startupTimeout: 300000
})
)
})

test('installPackage forwards server metadata for streamable HTTP packages', async () => {
const { service, mcpMock } = createService()

mcpMock.addServer.mockResolvedValue({
name: 'remote-server',
transportType: 'streamable_http',
url: 'https://mcp.example.com/mcp',
status: 'disconnected',
enabled: true
})

const result = await service.installPackage({
name: '@example/remote-mcp',
serverName: 'remote-server',
transportType: 'streamable_http',
url: 'https://mcp.example.com/mcp',
displayName: 'Remote MCP',
secretNames: ['bearerToken'],
secretFields: [
{
name: 'bearerToken',
label: 'Bearer token',
description: 'Bearer token used by the package.',
required: true,
inputType: 'password'
}
],
startupTimeout: 900000
})

expect(result.success).toBe(true)
expect(mcpMock.addServer).toHaveBeenCalledWith(
expect.objectContaining({
name: 'remote-server',
transportType: 'streamable_http',
url: 'https://mcp.example.com/mcp',
displayName: 'Remote MCP',
secretNames: ['bearerToken'],
secretFields: [
{
name: 'bearerToken',
label: 'Bearer token',
description: 'Bearer token used by the package.',
required: true,
inputType: 'password'
}
],
startupTimeout: 900000
})
)
})

test('upgradePackage uses pip for python runtime and does not update server command/args', async () => {
Expand Down
Loading