From ace28663d37920683f7cf42de12b0facf2b2506c Mon Sep 17 00:00:00 2001 From: Niketa Sharma Date: Mon, 20 Apr 2026 09:22:14 +0100 Subject: [PATCH] Align MCP with latest V1 API and bump to 0.1.13 --- README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src/__tests__/tools.test.ts | 18 +++++++++--------- src/index.ts | 4 ++-- src/setup.ts | 4 ++-- src/tools/oncall.ts | 4 ++-- src/tools/postmortems.ts | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 45739fd..b5275e3 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,7 @@ This MCP server follows the public Runframe direct API contract. - Incident creation depends on valid SLA configuration for the requested severity. If acknowledge or closure deadlines are missing, the API rejects the create. - Use `runframe_list_services` to discover valid `service_key` values before creating incidents. - `runframe_page_someone` supports the latest V1 public identifier flow: prefer `email`, with `user_id` still available when needed. -- Postmortem tools now follow the latest V1 contract: use `incident_number` and snake_case nested fields like `users_affected`, `owner_id`, and `time_to_acknowledge`. +- Postmortem tools now follow the latest V1 contract: use `incident_number` and snake_case nested fields like `users_affected`, `owner_email`, and `time_to_acknowledge`. - Use `runframe_find_user` to resolve a person name to an email address before filtering incidents by `assigned_to` or `resolved_by`. - Set `include_inactive=true` on `runframe_find_user` when you need to resolve former employees in historical incident queries. - Set `is_active=true` or `is_active=false` on `runframe_find_user` when you need an explicit V1 active-state filter. diff --git a/package-lock.json b/package-lock.json index 2a2d555..974a341 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@runframe/mcp-server", - "version": "0.1.12", + "version": "0.1.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@runframe/mcp-server", - "version": "0.1.12", + "version": "0.1.13", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.12.0", diff --git a/package.json b/package.json index deb74c3..43ac458 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@runframe/mcp-server", - "version": "0.1.12", + "version": "0.1.13", "description": "MCP server for Runframe incident management — any agent, any IDE, one system of record", "license": "MIT", "author": "Runframe (https://runframe.io)", diff --git a/src/__tests__/tools.test.ts b/src/__tests__/tools.test.ts index e30d037..af65e4d 100644 --- a/src/__tests__/tools.test.ts +++ b/src/__tests__/tools.test.ts @@ -361,11 +361,11 @@ describe('oncall tools', () => { assert.ok(call.path.startsWith('/api/v1/on-call/current')); }); - it('includes team_id when provided', async () => { - const teamId = '67555d9b-1087-4265-bfe6-28c214871862'; - await callTool(mcpClient, 'runframe_get_current_oncall', { team_id: teamId }); + it('includes team_name when provided', async () => { + const teamName = 'Platform'; + await callTool(mcpClient, 'runframe_get_current_oncall', { team_name: teamName }); const call = mock.lastCall(); - assert.ok(call.path.includes(`team_id=${teamId}`)); + assert.ok(call.path.includes(`team_name=${encodeURIComponent(teamName)}`)); }); it('returns the latest snake_case on-call payload unchanged', async () => { @@ -378,16 +378,15 @@ describe('oncall tools', () => { coverage_percentage: 100, }, services: [{ - service_id: 'service-1', + service_key: 'SER-00001', service_name: 'Payments API', service_description: null, - team_id: 'team-1', team_name: 'Platform', team_description: null, on_call_engineers: [], has_coverage: true, primary_on_call: null, - schedules: [], + schedule_names: [], }], }); @@ -395,7 +394,7 @@ describe('oncall tools', () => { const text = (result.content as Array<{ type: string; text: string }>)[0].text; const parsed = JSON.parse(text); assert.strictEqual(parsed.summary.total_services, 1); - assert.strictEqual(parsed.services[0].service_id, 'service-1'); + assert.strictEqual(parsed.services[0].service_key, 'SER-00001'); assert.strictEqual(parsed.services[0].has_coverage, true); }); }); @@ -482,7 +481,7 @@ describe('postmortem tools', () => { { timestamp: '2026-03-14T15:20:00Z', description: 'Resolved' }, ], action_items: [ - { text: 'Add connection pool monitoring', owner_id: 'user-1', due_date: '2026-04-30', status: 'pending' }, + { text: 'Add connection pool monitoring', owner_email: 'owner@example.com', due_date: '2026-04-30', status: 'pending' }, ], contributing_factors: 'No alerting on pool size', detection_path: 'Synthetic monitor', @@ -503,6 +502,7 @@ describe('postmortem tools', () => { assert.strictEqual(call.body?.root_cause, 'Connection pool exhausted'); assert.ok(Array.isArray(call.body?.timeline)); assert.ok(Array.isArray(call.body?.action_items)); + assert.strictEqual(call.body?.action_items?.[0]?.owner_email, 'owner@example.com'); }); it('works with incident_number only (minimum)', async () => { diff --git a/src/index.ts b/src/index.ts index c7c20a8..831a851 100644 --- a/src/index.ts +++ b/src/index.ts @@ -53,10 +53,10 @@ async function main() { // Verify key on startup try { - const verify = await client.get<{ valid: boolean; scopes: string[]; organizationName: string }>( + const verify = await client.get<{ valid: boolean; scopes: string[]; organization_name: string }>( '/api/v1/auth/verify' ); - console.error(`[runframe-mcp] Authenticated: ${verify.organizationName} (scopes: ${verify.scopes.join(', ')})`); + console.error(`[runframe-mcp] Authenticated: ${verify.organization_name} (scopes: ${verify.scopes.join(', ')})`); } catch (error) { if (error instanceof Error && 'status' in error) { const status = (error as { status: number }).status; diff --git a/src/setup.ts b/src/setup.ts index 3ea651e..9ad436f 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -40,11 +40,11 @@ export async function runSetup(apiUrl: string): Promise { // Validate against the API const client = new RunframeClient({ apiKey: key, apiUrl }); try { - const result = await client.get<{ valid: boolean; scopes: string[]; organizationName: string }>( + const result = await client.get<{ valid: boolean; scopes: string[]; organization_name: string }>( '/api/v1/auth/verify' ); console.error(''); - console.error(` ✓ Connected to ${result.organizationName} (scopes: ${result.scopes.join(', ')})`); + console.error(` ✓ Connected to ${result.organization_name} (scopes: ${result.scopes.join(', ')})`); validKey = key; } catch { console.error(' ✗ Could not verify key. Showing config with your key — double-check it at https://runframe.io/settings'); diff --git a/src/tools/oncall.ts b/src/tools/oncall.ts index ae7f77a..53e0f46 100644 --- a/src/tools/oncall.ts +++ b/src/tools/oncall.ts @@ -7,13 +7,13 @@ export function registerOncallTools(server: McpServer, client: RunframeClient) { server.registerTool('runframe_get_current_oncall', { description: 'Get the current on-call coverage.', inputSchema: { - team_id: z.string().uuid().optional().describe('Filter by team. If omitted, returns on-call for all teams.'), + team_name: z.string().min(1).optional().describe('Filter by exact team name. If omitted, returns on-call for all teams.'), }, annotations: { readOnlyHint: true, openWorldHint: true }, }, async (params) => { try { const query = new URLSearchParams(); - if (params.team_id) query.set('team_id', params.team_id); + if (params.team_name) query.set('team_name', params.team_name); const data = await client.get(`/api/v1/on-call/current?${query}`); return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] }; } catch (error) { return toolError(error, 'runframe_get_current_oncall'); } diff --git a/src/tools/postmortems.ts b/src/tools/postmortems.ts index 4a0c408..b0c9627 100644 --- a/src/tools/postmortems.ts +++ b/src/tools/postmortems.ts @@ -39,7 +39,7 @@ export function registerPostmortemTools(server: McpServer, client: RunframeClien })).optional().describe('Timeline of events'), action_items: z.array(z.object({ text: z.string(), - owner_id: z.string().optional(), + owner_email: z.string().email().optional(), due_date: z.string().optional(), status: z.enum(['pending', 'in_progress', 'completed']).default('pending'), })).optional().describe('Follow-up action items'),