Unified · Split
| @@ -29,18 +29,6 @@ · 28 unmodified lines | ||
| 29 | 29 | }, |
| 30 | 30 | "kind": "openapi" |
| 31 | 31 | }, |
| 32 | − "github-graphql": { | |
| 33 | − "name": "GitHub GraphQL", | |
| 34 | − "namespace": "github.graphql", | |
| 35 | − "config": { | |
| 36 | − "endpoint": "https://api.github.com/graphql", | |
| 37 | − "defaultHeaders": null, | |
| 38 | − "auth": { | |
| 39 | − "kind": "none" | |
| 40 | − } | |
| 41 | − }, | |
| 42 | − "kind": "graphql" | |
| 43 | − }, | |
| 44 | 32 | "github-graphql-2": { |
| 45 | 33 | "name": "GitHub GraphQL", |
| 46 | 34 | "namespace": "github.graphql", |
| @@ -61,7 +49,6 @@ · 14 unmodified lines | ||
| 61 | 49 | "axiom-mcp": { |
| 62 | 50 | "name": "Axiom MCP", |
| 63 | 51 | "namespace": "axiom.mcp", |
| 64 | − "kind": "mcp", | |
| 65 | 52 | "config": { |
| 66 | 53 | "endpoint": "https://mcp.axiom.co/mcp", |
| 67 | 54 | "transport": "auto", |
| @@ -129,6 +116,19 @@ · 61 unmodified lines | ||
| 129 | 116 | "client_id_issued_at": 1774829875 |
| 130 | 117 | } |
| 131 | 118 | } |
| 119 | + }, | |
| 120 | + "kind": "mcp" | |
| 121 | + }, | |
| 122 | + "linear-graphql": { | |
| 123 | + "name": "Linear GraphQL", | |
| 124 | + "namespace": "linear.graphql", | |
| 125 | + "kind": "graphql", | |
| 126 | + "config": { | |
| 127 | + "endpoint": "https://api.linear.app/graphql", | |
| 128 | + "defaultHeaders": null, | |
| 129 | + "auth": { | |
| 130 | + "kind": "none" | |
| 131 | + } | |
| 132 | 132 | } |
| 133 | 133 | } |
| 134 | 134 | } |
| @@ -1334,6 +1334,48 @@ describe("local-executor-server", () => { · 1333 unmodified lines | ||
| 1334 | 1334 | 15_000, |
| 1335 | 1335 | ); |
| 1336 | 1336 | |
| 1337 | + it.scoped("refreshes an MCP source through the API client", () => | |
| 1338 | + Effect.gen(function* () { | |
| 1339 | + const demoServer = yield* Effect.acquireRelease( | |
| 1340 | + Effect.promise(() => startMcpElicitationDemoServer()), | |
| 1341 | + (server) => Effect.promise(() => server.close()).pipe(Effect.orDie), | |
| 1342 | + ); | |
| 1343 | + const { installation, client } = yield* createApiClientHarness(); | |
| 1344 | + | |
| 1345 | + const created = yield* client.mcp.createSource({ | |
| 1346 | + path: { | |
| 1347 | + workspaceId: installation.scopeId, | |
| 1348 | + }, | |
| 1349 | + payload: { | |
| 1350 | + name: "Demo", | |
| 1351 | + endpoint: demoServer.endpoint, | |
| 1352 | + transport: "streamable-http", | |
| 1353 | + queryParams: null, | |
| 1354 | + headers: null, | |
| 1355 | + command: null, | |
| 1356 | + args: null, | |
| 1357 | + env: null, | |
| 1358 | + cwd: null, | |
| 1359 | + auth: { | |
| 1360 | + kind: "none", | |
| 1361 | + }, | |
| 1362 | + }, | |
| 1363 | + }); | |
| 1364 | + | |
| 1365 | + const refreshed = yield* client.mcp.refreshSource({ | |
| 1366 | + path: { | |
| 1367 | + workspaceId: installation.scopeId, | |
| 1368 | + sourceId: created.id, | |
| 1369 | + }, | |
| 1370 | + }); | |
| 1371 | + | |
| 1372 | + expect(refreshed.id).toBe(created.id); | |
| 1373 | + expect(refreshed.kind).toBe("mcp"); | |
| 1374 | + expect(refreshed.status).toBe("connected"); | |
| 1375 | + }), | |
| 1376 | + 15_000, | |
| 1377 | + ); | |
| 1378 | + | |
| 1337 | 1379 | it.scoped("can run the same MCP elicitation flow more than once without interaction id collisions", () => |
| 1338 | 1380 | Effect.gen(function* () { |
| 1339 | 1381 | const demoServer = yield* Effect.acquireRelease( |
| @@ -55,6 +55,9 @@ type McpExecutorExtension = { · 54 unmodified lines | ||
| 55 | 55 | updateSource: ( |
| 56 | 56 | input: McpUpdateSourceInput, |
| 57 | 57 | ) => Effect.Effect<Source, Error>; |
| 58 | + refreshSource: ( | |
| 59 | + sourceId: Source["id"], | |
| 60 | + ) => Effect.Effect<Source, Error>; | |
| 58 | 61 | removeSource: ( |
| 59 | 62 | sourceId: Source["id"], |
| 60 | 63 | ) => Effect.Effect<boolean, Error>; |
| @@ -117,6 +120,14 @@ export const McpHttpGroup = HttpApiGroup.make("mcp") · 56 unmodified lines | ||
| 117 | 120 | .addError(ControlPlaneNotFoundError) |
| 118 | 121 | .addError(ControlPlaneStorageError), |
| 119 | 122 | ) |
| 123 | + .add( | |
| 124 | + HttpApiEndpoint.post("refreshSource")`/workspaces/${workspaceIdParam}/plugins/mcp/sources/${sourceIdParam}/refresh` | |
| 125 | + .addSuccess(SourceSchema) | |
| 126 | + .addError(ControlPlaneBadRequestError) | |
| 127 | + .addError(ControlPlaneForbiddenError) | |
| 128 | + .addError(ControlPlaneNotFoundError) | |
| 129 | + .addError(ControlPlaneStorageError), | |
| 130 | + ) | |
| 120 | 131 | .add( |
| 121 | 132 | HttpApiEndpoint.del("removeSource")`/workspaces/${workspaceIdParam}/plugins/mcp/sources/${sourceIdParam}` |
| 122 | 133 | .addSuccess(Schema.Struct({ removed: Schema.Boolean })) |
| @@ -284,6 +295,17 @@ export const mcpHttpPlugin = (): ExecutorHttpPlugin< · 161 unmodified lines | ||
| 284 | 295 | ), |
| 285 | 296 | ) |
| 286 | 297 | ) |
| 298 | + .handle("refreshSource", ({ path }) => | |
| 299 | + resolveRequestedLocalWorkspace( | |
| 300 | + "mcp.refreshSource", | |
| 301 | + path.workspaceId, | |
| 302 | + ).pipe( | |
| 303 | + Effect.flatMap(() => executor.mcp.refreshSource(path.sourceId)), | |
| 304 | + Effect.mapError((cause) => | |
| 305 | + mapPluginStorageError("mcp.refreshSource", cause) | |
| 306 | + ), | |
| 307 | + ) | |
| 308 | + ) | |
| 287 | 309 | .handle("removeSource", ({ path }) => |
| 288 | 310 | resolveRequestedLocalWorkspace( |
| 289 | 311 | "mcp.removeSource", |
| @@ -733,6 +733,25 @@ export function McpDetailPage(props: { · 732 unmodified lines | ||
| 733 | 733 | client.mutation("mcp", "removeSource"), |
| 734 | 734 | { mode: "promise" }, |
| 735 | 735 | ); |
| 736 | + const refreshSource = useAtomSet( | |
| 737 | + client.mutation("mcp", "refreshSource"), | |
| 738 | + { mode: "promise" }, | |
| 739 | + ); | |
| 740 | + const refreshMutation = useExecutorMutation<Source["id"], Source>(async (sourceId) => | |
| 741 | + refreshSource({ | |
| 742 | + path: { | |
| 743 | + workspaceId: props.source.scopeId, | |
| 744 | + sourceId, | |
| 745 | + }, | |
| 746 | + reactivityKeys: { | |
| 747 | + sources: [props.source.scopeId], | |
| 748 | + source: [props.source.scopeId, sourceId], | |
| 749 | + sourceInspection: [props.source.scopeId, sourceId], | |
| 750 | + sourceInspectionTool: [props.source.scopeId, sourceId], | |
| 751 | + sourceDiscovery: [props.source.scopeId, sourceId], | |
| 752 | + }, | |
| 753 | + }) | |
| 754 | + ); | |
| 736 | 755 | const [confirmDelete, setConfirmDelete] = useState(false); |
| 737 | 756 | const [isDeleting, setIsDeleting] = useState(false); |
| 738 | 757 | const configResult = useAtomValue( |
| @@ -815,6 +834,19 @@ export function McpDetailPage(props: { · 76 unmodified lines | ||
| 815 | 834 | <> |
| 816 | 835 | <Button |
| 817 | 836 | variant="outline" |
| 837 | + size="sm" | |
| 838 | + type="button" | |
| 839 | + onClick={() => { | |
| 840 | + void refreshMutation.mutateAsync(props.source.id); | |
| 841 | + }} | |
| 842 | + disabled={refreshMutation.status === "pending"} | |
| 843 | + > | |
| 844 | + {refreshMutation.status === "pending" ? "Refreshing..." : "Refresh"} | |
| 845 | + </Button> | |
| 846 | + <Button | |
| 847 | + variant="outline" | |
| 848 | + size="sm" | |
| 849 | + type="button" | |
| 818 | 850 | onClick={() => |
| 819 | 851 | void navigation.edit(props.source.id)} |
| 820 | 852 | > |
| @@ -163,6 +163,9 @@ export type McpSdk = { · 162 unmodified lines | ||
| 163 | 163 | updateSource: ( |
| 164 | 164 | input: McpUpdateSourceInput, |
| 165 | 165 | ) => Effect.Effect<Source, Error>; |
| 166 | + refreshSource: ( | |
| 167 | + sourceId: Source["id"], | |
| 168 | + ) => Effect.Effect<Source, Error>; | |
| 166 | 169 | removeSource: ( |
| 167 | 170 | sourceId: Source["id"], |
| 168 | 171 | ) => Effect.Effect<boolean, Error>; |
| @@ -948,6 +951,13 @@ export const mcpSdkPlugin = ( · 779 unmodified lines | ||
| 948 | 951 | outputSchema: SourceSchema, |
| 949 | 952 | execute: ({ args, source }) => source.updateSource(args), |
| 950 | 953 | }, |
| 954 | + { | |
| 955 | + name: "refreshSource", | |
| 956 | + description: "Refresh an MCP source and resync its catalog.", | |
| 957 | + inputSchema: McpSourceIdInputSchema, | |
| 958 | + outputSchema: SourceSchema, | |
| 959 | + execute: ({ args, source }) => source.refreshSource(args.sourceId), | |
| 960 | + }, | |
| 951 | 961 | { |
| 952 | 962 | name: "removeSource", |
| 953 | 963 | description: "Remove an MCP source and its stored plugin data.", |
| @@ -1092,6 +1102,8 @@ export const mcpSdkPlugin = ( · 138 unmodified lines | ||
| 1092 | 1102 | provideRuntime(source.createSource(input)), |
| 1093 | 1103 | updateSource: (input) => |
| 1094 | 1104 | provideRuntime(source.updateSource(input)), |
| 1105 | + refreshSource: (sourceId) => | |
| 1106 | + provideRuntime(source.refreshSource(sourceId)), | |
| 1095 | 1107 | removeSource: (sourceId) => |
| 1096 | 1108 | provideRuntime(source.removeSource(sourceId)), |
| 1097 | 1109 | startOAuth: (input) => |