Skip to content

Commit 7c80c94

Browse files
cmsparksclaude
andauthored
chore: Worktree mcp tool annotations and oidc userinfo (#552)
* fix: type MCP tool arguments and add tool titles Inline sole component references into top-level tool arguments so each carries its own type, pattern, and enum. Infer an object type for discriminated unions and the machine pool request body. Add a title to every tool, emit destructiveHint on every tool, and describe the pools_create body. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(auth): support client metadata auth method lists * fix(auth): add OIDC identity support and proxy local MCP requests * docs: revert MCP deployment and architecture additions * chore: remove added OIDC explanatory comments * fix(auth): return invalid_scope on refresh and sign ID tokens without post-commit failures Validate the requested refresh scope against the granted scope before rotating so a mismatch yields invalid_scope instead of invalid_grant and never trips refresh token reuse detection. Load the OIDC signer before exchanging or refreshing and resolve the verified email inside the token transaction so nothing can fail after the code is consumed or tokens are committed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * Add tool annotations and oidc stuff --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 5c5c9d6 commit 7c80c94

24 files changed

Lines changed: 1155 additions & 168 deletions

‎frontend/apps/web/src/routes/OAuthAuthorize.test.tsx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ function oauthAuthorizeApi({
2626
redirect_host: '127.0.0.1:3000',
2727
loopback: true,
2828
resource: 'https://omnara.test/mcp',
29+
scope: 'openid email',
2930
}),
3031
approve = () => jsonResponse({ redirect_url: 'http://127.0.0.1:3000/callback?code=abc' }),
3132
deny = neverResponds,
@@ -92,6 +93,7 @@ describe('OAuth authorization decisions', () => {
9293
await renderOAuthAuthorize()
9394

9495
expect(container.textContent).toContain('Example MCP Client')
96+
expect(container.textContent).toContain('will also receive your verified email address')
9597
expect(container.textContent).toContain('127.0.0.1:3000')
9698
expect(container.textContent).toContain('redirects to a program running on your own computer')
9799
})

‎frontend/apps/web/src/routes/OAuthAuthorize.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@ export function OAuthAuthorize() {
106106
<Detail label="Client ID" value={state.request.clientId} />
107107
<Detail label="Redirects to" value={state.request.redirectHost} />
108108
</div>
109+
{state.request.scope.split(' ').includes('email') && (
110+
<p className="text-muted-foreground text-sm">
111+
This application will also receive your verified email address.
112+
</p>
113+
)}
109114
{state.request.loopback && (
110115
<p className="text-muted-foreground text-sm">
111116
This application redirects to a program running on your own computer. Only approve

‎frontend/apps/web/vite.config.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import react from '@vitejs/plugin-react'
55
import { defineConfig } from 'vite'
66

77
const apiTarget = process.env.OMNARA_API_PROXY ?? 'http://localhost:8080'
8-
const proxiedPrefixes = ['/api', '/install', '/.well-known']
8+
const proxiedPrefixes = ['/api', '/mcp', '/install', '/.well-known']
99
const proxy = Object.fromEntries(
1010
proxiedPrefixes.map((prefix) => [prefix, { target: apiTarget, changeOrigin: false, ws: true }]),
1111
)

‎frontend/packages/sdk/src/browser.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ export interface OAuthAuthorizePending {
179179
redirectHost: string
180180
loopback: boolean
181181
resource: string
182+
scope: string
182183
}
183184

184185
const zOAuthAuthorizePendingResponse = z.object({
@@ -189,6 +190,7 @@ const zOAuthAuthorizePendingResponse = z.object({
189190
redirect_host: z.string(),
190191
loopback: z.boolean(),
191192
resource: z.string(),
193+
scope: z.string().optional(),
192194
})
193195

194196
const zOAuthAuthorizeRedirectResponse = z.object({ redirect_url: z.string().min(1) })
@@ -237,6 +239,7 @@ export async function pendingOAuthAuthorization(query: string): Promise<OAuthAut
237239
redirectHost: data.redirect_host,
238240
loopback: data.loopback,
239241
resource: data.resource,
242+
scope: data.scope ?? '',
240243
}
241244
}
242245

‎gateway.nginx.conf‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ server {
1818
# The API enforces its own per-route body limits; stay above them.
1919
client_max_body_size 100m;
2020

21-
location ~ ^/(?:api|install|\.well-known|healthz)(?:/|$) {
21+
location ~ ^/(?:api|mcp|install|\.well-known|healthz)(?:/|$) {
2222
set $api_upstream http://api:8080;
2323
proxy_pass $api_upstream;
2424
proxy_http_version 1.1;

‎internal/httpapi/apimcp/manifest.go‎

Lines changed: 85 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -2,98 +2,108 @@ package apimcp
22

33
type Tool struct {
44
Name string
5+
Title string
56
OperationID string
67
Description string
78
Destructive bool
89
}
910

1011
var Tools = []Tool{
11-
{Name: "whoami", OperationID: "getCurrentUser"},
12+
{Name: "whoami", Title: "Who am I", OperationID: "getCurrentUser"},
1213

13-
{Name: "orgs_list", OperationID: "listOrganizations"},
14-
{Name: "orgs_create", OperationID: "createOrganization"},
15-
{Name: "orgs_overview", OperationID: "getOrgOverview"},
14+
{Name: "orgs_list", Title: "List organizations", OperationID: "listOrganizations"},
15+
{Name: "orgs_create", Title: "Create organization", OperationID: "createOrganization"},
16+
{Name: "orgs_overview", Title: "Get organization overview", OperationID: "getOrgOverview"},
1617

17-
{Name: "projects_list", OperationID: "listVisibleProjects"},
18-
{Name: "projects_create", OperationID: "createProject"},
18+
{Name: "projects_list", Title: "List projects", OperationID: "listVisibleProjects"},
19+
{Name: "projects_create", Title: "Create project", OperationID: "createProject"},
1920

20-
{Name: "agents_list", OperationID: "listAgents"},
21-
{Name: "agents_get", OperationID: "getAgent"},
22-
{Name: "agents_launch", OperationID: "createAgent"},
23-
{Name: "agents_update", OperationID: "updateAgentConfig"},
21+
{Name: "agents_list", Title: "List agents", OperationID: "listAgents"},
22+
{Name: "agents_get", Title: "Get agent", OperationID: "getAgent"},
23+
{Name: "agents_launch", Title: "Launch agent", OperationID: "createAgent"},
24+
{Name: "agents_update", Title: "Update agent config", OperationID: "updateAgentConfig", Destructive: true},
2425

25-
{Name: "configs_get", OperationID: "getAgentConfig"},
26-
{Name: "configs_create", OperationID: "createAgentConfig"},
27-
{Name: "agents_input", OperationID: "createAgentInput"},
28-
{Name: "agents_cancel", OperationID: "cancelAgent", Destructive: true},
29-
{Name: "agents_archive", OperationID: "archiveAgent", Destructive: true},
30-
{Name: "agents_events_list", OperationID: "listEvents"},
31-
{Name: "agents_interactions_list", OperationID: "listAgentInteractions"},
32-
{Name: "agents_interactions_resolve", OperationID: "resolveAgentInteraction"},
26+
{Name: "configs_get", Title: "Get agent config", OperationID: "getAgentConfig"},
27+
{Name: "configs_create", Title: "Create agent config", OperationID: "createAgentConfig"},
28+
{Name: "agents_input", Title: "Send agent input", OperationID: "createAgentInput"},
29+
{Name: "agents_cancel", Title: "Cancel agent", OperationID: "cancelAgent", Destructive: true},
30+
{Name: "agents_archive", Title: "Archive agent", OperationID: "archiveAgent", Destructive: true},
31+
{Name: "agents_events_list", Title: "List agent events", OperationID: "listEvents"},
32+
{Name: "agents_interactions_list", Title: "List agent interactions", OperationID: "listAgentInteractions"},
33+
{Name: "agents_interactions_resolve", Title: "Resolve agent interaction", OperationID: "resolveAgentInteraction"},
3334

34-
{Name: "machines_list", OperationID: "listVisibleMachines"},
35-
{Name: "machines_get", OperationID: "getMachine"},
36-
{Name: "machines_create", OperationID: "connectBYOMachine"},
37-
{Name: "machines_update", OperationID: "updateMachine"},
38-
{Name: "machines_delete", OperationID: "deleteMachine"},
35+
{Name: "machines_list", Title: "List machines", OperationID: "listVisibleMachines"},
36+
{Name: "machines_get", Title: "Get machine", OperationID: "getMachine"},
37+
{Name: "machines_create", Title: "Connect BYO machine", OperationID: "connectBYOMachine"},
38+
{Name: "machines_update", Title: "Update machine", OperationID: "updateMachine"},
39+
{Name: "machines_delete", Title: "Delete machine", OperationID: "deleteMachine"},
3940

40-
{Name: "pools_list", OperationID: "listMachinePools"},
41-
{Name: "pools_get", OperationID: "getMachinePool"},
42-
{Name: "pools_create", OperationID: "createMachinePool"},
43-
{Name: "pools_update", OperationID: "updateMachinePool"},
44-
{Name: "pools_delete", OperationID: "deleteMachinePool"},
41+
{Name: "pools_list", Title: "List machine pools", OperationID: "listMachinePools"},
42+
{Name: "pools_get", Title: "Get machine pool", OperationID: "getMachinePool"},
43+
{
44+
Name: "pools_create",
45+
Title: "Create machine pool",
46+
OperationID: "createMachinePool",
47+
Description: "Creates a machine pool in the organization. The body carries the pool name, the provider " +
48+
"(unikraft, modal, daytona, or blaxel), the provider auth secret, default machine settings, and " +
49+
"the per-machine and total CPU, memory, and machine-count limits the provider supports; the " +
50+
"required fields depend on the provider. See the createMachinePool operation in the Omnara API " +
51+
"reference at https://docs.omnara.com/api-reference for the full request schema.",
52+
},
53+
{Name: "pools_update", Title: "Update machine pool", OperationID: "updateMachinePool"},
54+
{Name: "pools_delete", Title: "Delete machine pool", OperationID: "deleteMachinePool"},
4555

46-
{Name: "model_providers_list", OperationID: "listModelProviderConfigs"},
47-
{Name: "model_providers_get", OperationID: "getModelProviderConfig"},
48-
{Name: "model_providers_create", OperationID: "createModelProviderConfig"},
49-
{Name: "model_providers_update", OperationID: "updateModelProviderConfig"},
50-
{Name: "model_providers_delete", OperationID: "deleteModelProviderConfig"},
51-
{Name: "model_providers_catalog", OperationID: "getModelCatalog"},
56+
{Name: "model_providers_list", Title: "List model provider configs", OperationID: "listModelProviderConfigs"},
57+
{Name: "model_providers_get", Title: "Get model provider config", OperationID: "getModelProviderConfig"},
58+
{Name: "model_providers_create", Title: "Create model provider config", OperationID: "createModelProviderConfig"},
59+
{Name: "model_providers_update", Title: "Update model provider config", OperationID: "updateModelProviderConfig"},
60+
{Name: "model_providers_delete", Title: "Delete model provider config", OperationID: "deleteModelProviderConfig"},
61+
{Name: "model_providers_catalog", Title: "Get model provider catalog", OperationID: "getModelCatalog"},
5262

53-
{Name: "models_list", OperationID: "listConfiguredModels"},
54-
{Name: "models_create", OperationID: "createConfiguredModel"},
55-
{Name: "models_update", OperationID: "updateConfiguredModel"},
56-
{Name: "models_delete", OperationID: "deleteConfiguredModel"},
63+
{Name: "models_list", Title: "List configured models", OperationID: "listConfiguredModels"},
64+
{Name: "models_create", Title: "Create configured model", OperationID: "createConfiguredModel"},
65+
{Name: "models_update", Title: "Update configured model", OperationID: "updateConfiguredModel"},
66+
{Name: "models_delete", Title: "Delete configured model", OperationID: "deleteConfiguredModel"},
5767

58-
{Name: "secrets_list", OperationID: "listSecrets"},
59-
{Name: "secrets_get", OperationID: "getSecret"},
60-
{Name: "secrets_create", OperationID: "createSecret"},
61-
{Name: "secrets_update", OperationID: "updateSecret"},
62-
{Name: "secrets_delete", OperationID: "deleteSecret"},
68+
{Name: "secrets_list", Title: "List secrets", OperationID: "listSecrets"},
69+
{Name: "secrets_get", Title: "Get secret", OperationID: "getSecret"},
70+
{Name: "secrets_create", Title: "Create secret", OperationID: "createSecret"},
71+
{Name: "secrets_update", Title: "Update secret", OperationID: "updateSecret"},
72+
{Name: "secrets_delete", Title: "Delete secret", OperationID: "deleteSecret"},
6373

64-
{Name: "skills_list", OperationID: "listSkills"},
65-
{Name: "skills_get", OperationID: "getSkill"},
66-
{Name: "skills_delete", OperationID: "deleteSkill"},
74+
{Name: "skills_list", Title: "List skills", OperationID: "listSkills"},
75+
{Name: "skills_get", Title: "Get skill", OperationID: "getSkill"},
76+
{Name: "skills_delete", Title: "Delete skill", OperationID: "deleteSkill"},
6777

68-
{Name: "profiles_list", OperationID: "listAgentProfiles"},
69-
{Name: "profiles_get", OperationID: "getAgentProfile"},
70-
{Name: "profiles_create", OperationID: "createAgentProfile"},
71-
{Name: "profiles_update", OperationID: "updateAgentProfile"},
72-
{Name: "profiles_rename", OperationID: "renameAgentProfile"},
73-
{Name: "profiles_delete", OperationID: "deleteAgentProfile"},
78+
{Name: "profiles_list", Title: "List agent profiles", OperationID: "listAgentProfiles"},
79+
{Name: "profiles_get", Title: "Get agent profile", OperationID: "getAgentProfile"},
80+
{Name: "profiles_create", Title: "Create agent profile", OperationID: "createAgentProfile"},
81+
{Name: "profiles_update", Title: "Update agent profile", OperationID: "updateAgentProfile", Destructive: true},
82+
{Name: "profiles_rename", Title: "Rename agent profile", OperationID: "renameAgentProfile"},
83+
{Name: "profiles_delete", Title: "Delete agent profile", OperationID: "deleteAgentProfile"},
7484

75-
{Name: "crons_list", OperationID: "listCronTriggers"},
76-
{Name: "crons_get", OperationID: "getCronTrigger"},
77-
{Name: "crons_create", OperationID: "createCronTrigger"},
78-
{Name: "crons_update", OperationID: "updateCronTrigger"},
79-
{Name: "crons_delete", OperationID: "deleteCronTrigger"},
85+
{Name: "crons_list", Title: "List cron triggers", OperationID: "listCronTriggers"},
86+
{Name: "crons_get", Title: "Get cron trigger", OperationID: "getCronTrigger"},
87+
{Name: "crons_create", Title: "Create cron trigger", OperationID: "createCronTrigger"},
88+
{Name: "crons_update", Title: "Update cron trigger", OperationID: "updateCronTrigger"},
89+
{Name: "crons_delete", Title: "Delete cron trigger", OperationID: "deleteCronTrigger"},
8090

81-
{Name: "grant_skills_list", OperationID: "listSkillGrants"},
82-
{Name: "grant_skills_add", OperationID: "createSkillGrant"},
83-
{Name: "grant_skills_delete", OperationID: "deleteSkillGrant"},
84-
{Name: "grant_secrets_list", OperationID: "listSecretGrants"},
85-
{Name: "grant_secrets_add", OperationID: "createSecretGrant"},
86-
{Name: "grant_secrets_delete", OperationID: "deleteSecretGrant"},
87-
{Name: "grant_machines_list", OperationID: "listProjectMachineGrants"},
88-
{Name: "grant_machines_add", OperationID: "createProjectMachineGrant"},
89-
{Name: "grant_machines_delete", OperationID: "deleteProjectMachineGrant"},
90-
{Name: "grant_pools_list", OperationID: "listProjectMachinePoolGrants"},
91-
{Name: "grant_pools_get", OperationID: "getProjectMachinePoolGrant"},
92-
{Name: "grant_pools_add", OperationID: "createProjectMachinePoolGrant"},
93-
{Name: "grant_pools_edit", OperationID: "updateProjectMachinePoolGrant"},
94-
{Name: "grant_pools_delete", OperationID: "deleteProjectMachinePoolGrant"},
95-
{Name: "grant_models_list", OperationID: "listProjectModelGrants"},
96-
{Name: "grant_models_add", OperationID: "createProjectModelGrant"},
97-
{Name: "grant_models_edit", OperationID: "updateProjectModelGrant"},
98-
{Name: "grant_models_delete", OperationID: "deleteProjectModelGrant"},
91+
{Name: "grant_skills_list", Title: "List skill grants", OperationID: "listSkillGrants"},
92+
{Name: "grant_skills_add", Title: "Grant skill to project", OperationID: "createSkillGrant"},
93+
{Name: "grant_skills_delete", Title: "Delete skill grant", OperationID: "deleteSkillGrant"},
94+
{Name: "grant_secrets_list", Title: "List secret grants", OperationID: "listSecretGrants"},
95+
{Name: "grant_secrets_add", Title: "Grant secret to project", OperationID: "createSecretGrant"},
96+
{Name: "grant_secrets_delete", Title: "Delete secret grant", OperationID: "deleteSecretGrant"},
97+
{Name: "grant_machines_list", Title: "List project machine grants", OperationID: "listProjectMachineGrants"},
98+
{Name: "grant_machines_add", Title: "Grant machine to project", OperationID: "createProjectMachineGrant"},
99+
{Name: "grant_machines_delete", Title: "Delete project machine grant", OperationID: "deleteProjectMachineGrant"},
100+
{Name: "grant_pools_list", Title: "List project machine pool grants", OperationID: "listProjectMachinePoolGrants"},
101+
{Name: "grant_pools_get", Title: "Get project machine pool grant", OperationID: "getProjectMachinePoolGrant"},
102+
{Name: "grant_pools_add", Title: "Grant machine pool to project", OperationID: "createProjectMachinePoolGrant"},
103+
{Name: "grant_pools_edit", Title: "Update project machine pool grant", OperationID: "updateProjectMachinePoolGrant"},
104+
{Name: "grant_pools_delete", Title: "Delete project machine pool grant", OperationID: "deleteProjectMachinePoolGrant"},
105+
{Name: "grant_models_list", Title: "List project model grants", OperationID: "listProjectModelGrants"},
106+
{Name: "grant_models_add", Title: "Grant model to project", OperationID: "createProjectModelGrant"},
107+
{Name: "grant_models_edit", Title: "Update project model grant", OperationID: "updateProjectModelGrant"},
108+
{Name: "grant_models_delete", Title: "Delete project model grant", OperationID: "deleteProjectModelGrant"},
99109
}

0 commit comments

Comments
 (0)