Unified · Split
| @@ -29,18 +29,6 @@ · 28 unmodified lines | |||
| 29 | }, | 29 | }, |
| 30 | "kind": "openapi" | 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 | "github-graphql-2": { | 32 | "github-graphql-2": { |
| 45 | "name": "GitHub GraphQL", | 33 | "name": "GitHub GraphQL", |
| 46 | "namespace": "github.graphql", | 34 | "namespace": "github.graphql", |
| @@ -61,7 +49,6 @@ · 14 unmodified lines | |||
| 61 | "axiom-mcp": { | 49 | "axiom-mcp": { |
| 62 | "name": "Axiom MCP", | 50 | "name": "Axiom MCP", |
| 63 | "namespace": "axiom.mcp", | 51 | "namespace": "axiom.mcp", |
| 64 | "kind": "mcp", | ||
| 65 | "config": { | 52 | "config": { |
| 66 | "endpoint": "https://mcp.axiom.co/mcp", | 53 | "endpoint": "https://mcp.axiom.co/mcp", |
| 67 | "transport": "auto", | 54 | "transport": "auto", |
| @@ -129,6 +116,19 @@ · 61 unmodified lines | |||
| 129 | "client_id_issued_at": 1774829875 | 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 | 15_000, | 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 | it.scoped("can run the same MCP elicitation flow more than once without interaction id collisions", () => | 1379 | it.scoped("can run the same MCP elicitation flow more than once without interaction id collisions", () => |
| 1338 | Effect.gen(function* () { | 1380 | Effect.gen(function* () { |
| 1339 | const demoServer = yield* Effect.acquireRelease( | 1381 | const demoServer = yield* Effect.acquireRelease( |
| @@ -55,6 +55,9 @@ type McpExecutorExtension = { · 54 unmodified lines | |||
| 55 | updateSource: ( | 55 | updateSource: ( |
| 56 | input: McpUpdateSourceInput, | 56 | input: McpUpdateSourceInput, |
| 57 | ) => Effect.Effect<Source, Error>; | 57 | ) => Effect.Effect<Source, Error>; |
| 58 | refreshSource: ( | ||
| 59 | sourceId: Source["id"], | ||
| 60 | ) => Effect.Effect<Source, Error>; | ||
| 58 | removeSource: ( | 61 | removeSource: ( |
| 59 | sourceId: Source["id"], | 62 | sourceId: Source["id"], |
| 60 | ) => Effect.Effect<boolean, Error>; | 63 | ) => Effect.Effect<boolean, Error>; |
| @@ -117,6 +120,14 @@ export const McpHttpGroup = HttpApiGroup.make("mcp") · 56 unmodified lines | |||
| 117 | .addError(ControlPlaneNotFoundError) | 120 | .addError(ControlPlaneNotFoundError) |
| 118 | .addError(ControlPlaneStorageError), | 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 | .add( | 131 | .add( |
| 121 | HttpApiEndpoint.del("removeSource")`/workspaces/${workspaceIdParam}/plugins/mcp/sources/${sourceIdParam}` | 132 | HttpApiEndpoint.del("removeSource")`/workspaces/${workspaceIdParam}/plugins/mcp/sources/${sourceIdParam}` |
| 122 | .addSuccess(Schema.Struct({ removed: Schema.Boolean })) | 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 | .handle("removeSource", ({ path }) => | 309 | .handle("removeSource", ({ path }) => |
| 288 | resolveRequestedLocalWorkspace( | 310 | resolveRequestedLocalWorkspace( |
| 289 | "mcp.removeSource", | 311 | "mcp.removeSource", |
| @@ -733,6 +733,25 @@ export function McpDetailPage(props: { · 732 unmodified lines | |||
| 733 | client.mutation("mcp", "removeSource"), | 733 | client.mutation("mcp", "removeSource"), |
| 734 | { mode: "promise" }, | 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 | const [confirmDelete, setConfirmDelete] = useState(false); | 755 | const [confirmDelete, setConfirmDelete] = useState(false); |
| 737 | const [isDeleting, setIsDeleting] = useState(false); | 756 | const [isDeleting, setIsDeleting] = useState(false); |
| 738 | const configResult = useAtomValue( | 757 | const configResult = useAtomValue( |
| @@ -815,6 +834,19 @@ export function McpDetailPage(props: { · 76 unmodified lines | |||
| 815 | <> | 834 | <> |
| 816 | <Button | 835 | <Button |
| 817 | variant="outline" | 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 | onClick={() => | 850 | onClick={() => |
| 819 | void navigation.edit(props.source.id)} | 851 | void navigation.edit(props.source.id)} |
| 820 | > | 852 | > |
| @@ -163,6 +163,9 @@ export type McpSdk = { · 162 unmodified lines | |||
| 163 | updateSource: ( | 163 | updateSource: ( |
| 164 | input: McpUpdateSourceInput, | 164 | input: McpUpdateSourceInput, |
| 165 | ) => Effect.Effect<Source, Error>; | 165 | ) => Effect.Effect<Source, Error>; |
| 166 | refreshSource: ( | ||
| 167 | sourceId: Source["id"], | ||
| 168 | ) => Effect.Effect<Source, Error>; | ||
| 166 | removeSource: ( | 169 | removeSource: ( |
| 167 | sourceId: Source["id"], | 170 | sourceId: Source["id"], |
| 168 | ) => Effect.Effect<boolean, Error>; | 171 | ) => Effect.Effect<boolean, Error>; |
| @@ -948,6 +951,13 @@ export const mcpSdkPlugin = ( · 779 unmodified lines | |||
| 948 | outputSchema: SourceSchema, | 951 | outputSchema: SourceSchema, |
| 949 | execute: ({ args, source }) => source.updateSource(args), | 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 | name: "removeSource", | 962 | name: "removeSource", |
| 953 | description: "Remove an MCP source and its stored plugin data.", | 963 | description: "Remove an MCP source and its stored plugin data.", |
| @@ -1092,6 +1102,8 @@ export const mcpSdkPlugin = ( · 138 unmodified lines | |||
| 1092 | provideRuntime(source.createSource(input)), | 1102 | provideRuntime(source.createSource(input)), |
| 1093 | updateSource: (input) => | 1103 | updateSource: (input) => |
| 1094 | provideRuntime(source.updateSource(input)), | 1104 | provideRuntime(source.updateSource(input)), |
| 1105 | refreshSource: (sourceId) => | ||
| 1106 | provideRuntime(source.refreshSource(sourceId)), | ||
| 1095 | removeSource: (sourceId) => | 1107 | removeSource: (sourceId) => |
| 1096 | provideRuntime(source.removeSource(sourceId)), | 1108 | provideRuntime(source.removeSource(sourceId)), |
| 1097 | startOAuth: (input) => | 1109 | startOAuth: (input) => |