diff --git a/package.json b/package.json index bc6250d..1c04927 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/services/packages.ts b/src/services/packages.ts index f54c45a..7af9430 100644 --- a/src/services/packages.ts +++ b/src/services/packages.ts @@ -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' @@ -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 @@ -47,7 +62,6 @@ export interface InstallPackageRequest { url?: string headers?: Record reconnectionOptions?: StreamableHTTPReconnectionOptions - secretName?: string enabled?: boolean failOnWarning?: boolean runtime?: PackageRuntime @@ -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 + } + } + private async ensureVenv(pythonExecutable: string, venvAbsolutePath: string): Promise { const pythonPath = this.venvPythonPath(venvAbsolutePath) if (existsSync(pythonPath)) { @@ -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) { @@ -364,7 +394,7 @@ export class PackageService { command: pythonCommand, args: pythonArgs, env: pythonEnv, - secretName, + ...serverMetadata, enabled }) @@ -483,7 +513,7 @@ export class PackageService { url: url!, headers, reconnectionOptions, - secretName, + ...serverMetadata, enabled }) } else { @@ -546,7 +576,7 @@ export class PackageService { command: finalCommand, args: stdioArgs, env: stdioEnv, - secretName, + ...serverMetadata, enabled }) } diff --git a/test/packages-python.spec.ts b/test/packages-python.spec.ts index e7455e7..cbefa80 100644 --- a/test/packages-python.spec.ts +++ b/test/packages-python.spec.ts @@ -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) @@ -173,6 +191,16 @@ describe('PackageService python runtime support', () => { command: string args: string[] env: Record + 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( @@ -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', + 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 () => {