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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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)",
Expand Down
18 changes: 9 additions & 9 deletions src/__tests__/tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -378,24 +378,23 @@ 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: [],
}],
});

const result = await callTool(mcpClient, 'runframe_get_current_oncall', {});
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);
});
});
Expand Down Expand Up @@ -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',
Expand All @@ -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 () => {
Expand Down
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ export async function runSetup(apiUrl: string): Promise<void> {
// 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');
Expand Down
4 changes: 2 additions & 2 deletions src/tools/oncall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'); }
Expand Down
2 changes: 1 addition & 1 deletion src/tools/postmortems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Loading