History

oauth

Rhys Sullivan committed 8 months ago

Commit aa596e21 parent 477f8054 files · +66 −9Browse files

Unified · Split

4 files
  1. apps/executor/src/server/server.test.ts +50 −4
  2. apps/web/src/views/add-source.tsx +5 −1
  3. apps/web/src/views/source-editor.tsx +5 −3
  4. packages/control-plane/src/runtime/graphql-tools.ts +6 −1
apps/executor/src/server/server.test.ts+50−4
@@ -107,10 +107,12 @@ type ExecutorMcpClient = { · 106 unmodified lines
107 close: () => Promise<void>;107 close: () => Promise<void>;
108};108};
109 109
110type ElicitationHandler = NonNullable<Parameters<Client["setRequestHandler"]>[1]>;
111
110const makeExecutorMcpClient = (input: {112const makeExecutorMcpClient = (input: {
111 baseUrl: string;113 baseUrl: string;
112 capabilities?: Record<string, unknown>;114 capabilities?: Record<string, unknown>;
113 onElicitation?: (request: { params: { message?: string } }) => Promise<unknown>;115 onElicitation?: ElicitationHandler;
114}) =>116}) =>
115 Effect.acquireRelease(117 Effect.acquireRelease(
116 Effect.promise<ExecutorMcpClient>(async () => {118 Effect.promise<ExecutorMcpClient>(async () => {
@@ -898,6 +900,11 @@ describe("local-executor-server", () => { · 781 unmodified lines
898 900
899 it.scoped("updates an existing demo MCP source instead of creating a duplicate", () =>901 it.scoped("updates an existing demo MCP source instead of creating a duplicate", () =>
900 Effect.gen(function* () {902 Effect.gen(function* () {
903 const demoServer = yield* Effect.acquireRelease(
904 Effect.promise(() => startMcpElicitationDemoServer()),
905 (server) => Effect.promise(() => server.close()).pipe(Effect.orDie),
906 );
907
901 const server = yield* createLocalExecutorServer({908 const server = yield* createLocalExecutorServer({
902 port: 0,909 port: 0,
903 localDataDir: ":memory:",910 localDataDir: ":memory:",
@@ -933,7 +940,7 @@ describe("local-executor-server", () => { · 29 unmodified lines
933 const seeded = yield* seedDemoMcpSourceInWorkspace({940 const seeded = yield* seedDemoMcpSourceInWorkspace({
934 client,941 client,
935 workspaceId: installation.workspaceId,942 workspaceId: installation.workspaceId,
936 endpoint: "http://127.0.0.1:58506/mcp",943 endpoint: demoServer.endpoint,
937 name: "Demo",944 name: "Demo",
938 namespace: "demo",945 namespace: "demo",
939 });946 });
@@ -948,7 +955,7 @@ describe("local-executor-server", () => { · 8 unmodified lines
948 });955 });
949 956
950 expect(sources).toHaveLength(1);957 expect(sources).toHaveLength(1);
951 expect(sources[0]?.endpoint).toBe("http://127.0.0.1:58506/mcp");958 expect(sources[0]?.endpoint).toBe(demoServer.endpoint);
952 }),959 }),
953 );960 );
954 961
@@ -1295,9 +1302,10 @@ describe("local-executor-server", () => { · 340 unmodified lines
1295 accountId: installation.accountId,1302 accountId: installation.accountId,
1296 });1303 });
1297 1304
1305 const sourceId = SourceIdSchema.make("src_invalid_mcp_endpoint");
1298 const now = Date.now();1306 const now = Date.now();
1299 yield* server.runtime.persistence.rows.sources.insert({1307 yield* server.runtime.persistence.rows.sources.insert({
1300 id: SourceIdSchema.make("src_invalid_mcp_endpoint"),1308 id: sourceId,
1301 workspaceId: installation.workspaceId,1309 workspaceId: installation.workspaceId,
1302 name: "Demo",1310 name: "Demo",
1303 kind: "mcp",1311 kind: "mcp",
@@ -1316,6 +1324,44 @@ describe("local-executor-server", () => { · 12 unmodified lines
1316 createdAt: now,1324 createdAt: now,
1317 updatedAt: now,1325 updatedAt: now,
1318 });1326 });
1327 yield* server.runtime.persistence.rows.toolArtifacts.replaceForSource({
1328 workspaceId: installation.workspaceId,
1329 sourceId,
1330 artifacts: [{
1331 artifact: {
1332 workspaceId: installation.workspaceId,
1333 path: "demo.gated_echo",
1334 toolId: "gated_echo",
1335 sourceId,
1336 title: "gated_echo",
1337 description: "Asks for approval before echoing a value",
1338 searchNamespace: "demo.gated_echo",
1339 searchText: "demo.gated_echo gated echo approval",
1340 inputSchemaJson: JSON.stringify({
1341 type: "object",
1342 properties: {
1343 value: {
1344 type: "string",
1345 },
1346 },
1347 required: ["value"],
1348 additionalProperties: false,
1349 }),
1350 outputSchemaJson: null,
1351 providerKind: "mcp",
1352 mcpToolName: "gated_echo",
1353 openApiMethod: null,
1354 openApiPathTemplate: null,
1355 openApiOperationHash: null,
1356 openApiRawToolId: null,
1357 openApiOperationId: null,
1358 openApiTagsJson: null,
1359 openApiRequestBodyRequired: null,
1360 createdAt: now,
1361 updatedAt: now,
1362 },
1363 }],
1364 });
1319 1365
1320 const failure = yield* client.executions1366 const failure = yield* client.executions
1321 .create({1367 .create({
apps/web/src/views/add-source.tsx+5−1
@@ -441,6 +441,10 @@ const startSourceOAuthPopup = async (input: { · 440 unmodified lines
441 return;441 return;
442 }442 }
443 if (popup.closed) {443 if (popup.closed) {
444 // Stop polling — only run one final deferred check to give the
445 // callback page time to write localStorage before we give up.
446 window.clearInterval(closedPoll);
447 closedPoll = 0;
444 window.setTimeout(() => {448 window.setTimeout(() => {
445 const delayedStored = readStoredSourceOAuthPopupResult(input.sessionId);449 const delayedStored = readStoredSourceOAuthPopupResult(input.sessionId);
446 if (delayedStored) {450 if (delayedStored) {
@@ -448,7 +452,7 @@ const startSourceOAuthPopup = async (input: { · 1 unmodified line
448 return;452 return;
449 }453 }
450 settleWithError("OAuth popup was closed before completion.");454 settleWithError("OAuth popup was closed before completion.");
451 }, 400);455 }, 1500);
452 }456 }
453 }, 300);457 }, 300);
454 });458 });
apps/web/src/views/source-editor.tsx+5−3
@@ -232,17 +232,19 @@ const startSourceOAuthPopup = async (input: { · 231 unmodified lines
232 settleFromPayload(stored);232 settleFromPayload(stored);
233 return;233 return;
234 }234 }
235
236 if (popup.closed) {235 if (popup.closed) {
236 // Stop polling — only run one final deferred check to give the
237 // callback page time to write localStorage before we give up.
238 window.clearInterval(closedPoll);
239 closedPoll = 0;
237 window.setTimeout(() => {240 window.setTimeout(() => {
238 const delayedStored = readStoredSourceOAuthPopupResult(input.sessionId);241 const delayedStored = readStoredSourceOAuthPopupResult(input.sessionId);
239 if (delayedStored) {242 if (delayedStored) {
240 settleFromPayload(delayedStored);243 settleFromPayload(delayedStored);
241 return;244 return;
242 }245 }
243
244 settleWithError("OAuth popup was closed before completion.");246 settleWithError("OAuth popup was closed before completion.");
245 }, 400);247 }, 1500);
246 }248 }
247 }, 300);249 }, 300);
248 });250 });
packages/control-plane/src/runtime/graphql-tools.ts+6−1
@@ -1116,9 +1116,14 @@ const invokeGraphqlHttpRequest = (input: GraphqlHttpInvocation) => · 1115 unmodified lines
1116 catch: (cause) => graphqlToolError(`Failed decoding GraphQL response for ${input.path}`, cause),1116 catch: (cause) => graphqlToolError(`Failed decoding GraphQL response for ${input.path}`, cause),
1117 });1117 });
1118 1118
1119 const responseHeaders: Record<string, string> = {};
1120 response.headers.forEach((value, key) => {
1121 responseHeaders[key] = value;
1122 });
1123
1119 return {1124 return {
1120 status: response.status,1125 status: response.status,
1121 headers: sanitizeResponseHeaders(Object.fromEntries(response.headers)),1126 headers: sanitizeResponseHeaders(responseHeaders),
1122 body,1127 body,
1123 isError: response.status >= 400 || hasGraphqlErrors(body),1128 isError: response.status >= 400 || hasGraphqlErrors(body),
1124 };1129 };