Skip to content

Commit d35cb4c

Browse files
authored
Merge pull request #731 from RhysSullivan/codex/fix-mcp-oauth-openapi-parsing
[codex] Fix MCP OAuth and OpenAPI JSON parsing
2 parents f8fc3b4 + 4e60892 commit d35cb4c

6 files changed

Lines changed: 75 additions & 27 deletions

File tree

‎packages/plugins/mcp/src/api/group.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ const ProbeEndpointPayload = Schema.Struct({
8888
const ProbeEndpointResponse = Schema.Struct({
8989
connected: Schema.Boolean,
9090
requiresOAuth: Schema.Boolean,
91+
supportsDynamicRegistration: Schema.Boolean,
9192
name: Schema.String,
9293
namespace: Schema.String,
9394
toolCount: Schema.NullOr(Schema.Number),

‎packages/plugins/mcp/src/react/AddMcpSource.tsx‎

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ type OAuthTokens = OAuthCompletionPayload;
7979
type ProbeResult = {
8080
connected: boolean;
8181
requiresOAuth: boolean;
82+
supportsDynamicRegistration: boolean;
8283
name: string;
8384
namespace: string;
8485
toolCount: number | null;
@@ -315,7 +316,7 @@ export default function AddMcpSource(props: {
315316
const isAdding = state.step === "adding";
316317
const isOAuthBusy =
317318
state.step === "oauth-starting" || state.step === "oauth-waiting" || oauth.busy;
318-
const canUseNone = probe?.requiresOAuth !== true;
319+
const canUseNone = probe?.requiresOAuth !== true || probe.supportsDynamicRegistration === false;
319320
const remoteHeadersComplete = remoteHeaders.every(
320321
(header) => header.name.trim() && header.value.trim(),
321322
);
@@ -622,7 +623,7 @@ export default function AddMcpSource(props: {
622623
<FieldLabel>Authentication</FieldLabel>
623624
<FilterTabs<RemoteAuthMode>
624625
tabs={
625-
probe.requiresOAuth
626+
probe.requiresOAuth && probe.supportsDynamicRegistration
626627
? [{ value: "oauth2", label: "OAuth" }]
627628
: [
628629
{ value: "none", label: "None" },
@@ -649,16 +650,24 @@ export default function AddMcpSource(props: {
649650
label="Connect via OAuth"
650651
help="Start the provider OAuth flow."
651652
>
652-
{!tokens && state.step === "probed" && (
653-
<Button
654-
type="button"
655-
onClick={handleOAuth}
656-
variant="outline"
657-
className="w-full"
658-
>
659-
Sign in
660-
</Button>
661-
)}
653+
{!tokens &&
654+
state.step === "probed" &&
655+
(probe.supportsDynamicRegistration ? (
656+
<Button
657+
type="button"
658+
onClick={handleOAuth}
659+
variant="outline"
660+
className="w-full"
661+
>
662+
Sign in
663+
</Button>
664+
) : (
665+
<div className="rounded-md border border-border bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
666+
This server requires OAuth, but its authorization server does not support
667+
dynamic client registration. Use request headers with a bearer token, or
668+
save the source and connect a supported OAuth connection later.
669+
</div>
670+
))}
662671

663672
{!tokens && state.step === "oauth-starting" && (
664673
<div className="flex min-h-9 items-center gap-2 rounded-md border border-border bg-muted/30 px-3 py-2">

‎packages/plugins/mcp/src/sdk/plugin.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ export type McpSourceConfig = McpRemoteSourceConfig | McpStdioSourceConfig;
122122
export interface McpProbeResult {
123123
readonly connected: boolean;
124124
readonly requiresOAuth: boolean;
125+
readonly supportsDynamicRegistration: boolean;
125126
readonly name: string;
126127
readonly namespace: string;
127128
readonly toolCount: number | null;
@@ -1019,6 +1020,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
10191020
return {
10201021
connected: true,
10211022
requiresOAuth: false,
1023+
supportsDynamicRegistration: false,
10221024
name: result.manifest.server?.name ?? name,
10231025
namespace,
10241026
toolCount: result.manifest.tools.length,
@@ -1054,15 +1056,16 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => {
10541056
queryParams: probeQueryParams,
10551057
})
10561058
.pipe(
1057-
Effect.map(() => true),
1058-
Effect.catch(() => Effect.succeed(false)),
1059+
Effect.map((oauth) => ({ ok: true as const, oauth })),
1060+
Effect.catch(() => Effect.succeed({ ok: false as const, oauth: null })),
10591061
Effect.withSpan("mcp.plugin.probe_oauth"),
10601062
);
10611063

1062-
if (probeResult) {
1064+
if (probeResult.ok) {
10631065
return {
10641066
connected: false,
10651067
requiresOAuth: true,
1068+
supportsDynamicRegistration: probeResult.oauth.supportsDynamicRegistration,
10661069
name,
10671070
namespace,
10681071
toolCount: null,

‎packages/plugins/openapi/src/sdk/parse.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ paths: {}
3333
}),
3434
);
3535

36+
it.effect("falls back to YAML for flow-style YAML documents", () =>
37+
Effect.gen(function* () {
38+
const doc = yield* parse(`
39+
{
40+
openapi: 3.0.0,
41+
info: { title: Test, version: 1.0.0 },
42+
paths: {}
43+
}
44+
`);
45+
46+
expect(doc.openapi).toBe("3.0.0");
47+
}),
48+
);
49+
3650
it.effect("returns a stable parse error for empty documents", () =>
3751
Effect.gen(function* () {
3852
const error = yield* parse("").pipe(Effect.flip);

‎packages/plugins/openapi/src/sdk/parse.ts‎

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { OpenAPI, OpenAPIV3, OpenAPIV3_1 } from "openapi-types";
2-
import { Duration, Effect } from "effect";
2+
import { Duration, Effect, Schema } from "effect";
33
import { HttpClient, HttpClientRequest } from "effect/unstable/http";
44
import YAML from "yaml";
55

@@ -107,13 +107,14 @@ const parseTextToObject = (text: string): Effect.Effect<OpenAPI.Document, OpenAp
107107
});
108108
}
109109

110-
const parsed = yield* Effect.try({
111-
try: () => YAML.parse(trimmed) as unknown,
112-
catch: () =>
113-
new OpenApiParseError({
114-
message: "Failed to parse OpenAPI document",
115-
}),
116-
});
110+
const parsed = yield* parseJsonLike(trimmed).pipe(
111+
Effect.mapError(
112+
() =>
113+
new OpenApiParseError({
114+
message: "Failed to parse OpenAPI document",
115+
}),
116+
),
117+
);
117118

118119
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
119120
return yield* new OpenApiParseError({
@@ -123,3 +124,14 @@ const parseTextToObject = (text: string): Effect.Effect<OpenAPI.Document, OpenAp
123124

124125
return parsed as OpenAPI.Document;
125126
});
127+
128+
const parseJsonText = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Unknown));
129+
130+
const parseJsonLike = (text: string): Effect.Effect<unknown, unknown> => {
131+
const parseYaml = Effect.try({
132+
try: () => YAML.parse(text) as unknown,
133+
catch: () => "YamlParseFailed" as const,
134+
});
135+
if (!text.startsWith("{") && !text.startsWith("[")) return parseYaml;
136+
return parseJsonText(text).pipe(Effect.catch(() => parseYaml));
137+
};

‎packages/react/src/plugins/oauth-sign-in.tsx‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as Effect from "effect/Effect";
55
import * as Exit from "effect/Exit";
66

77
import { cancelOAuth, startOAuth } from "../api/atoms";
8-
import { messageFromUnknown, useReportHandledError } from "../api/error-reporting";
8+
import { messageFromExit, messageFromUnknown, useReportHandledError } from "../api/error-reporting";
99
import { openOAuthPopup, reserveOAuthPopup, type OAuthPopupResult } from "../api/oauth-popup";
1010
import { Button } from "../components/button";
1111
import {
@@ -48,6 +48,7 @@ export type OAuthAuthorizationStartResult = {
4848

4949
class OAuthAuthorizationStartError extends Data.TaggedError("OAuthAuthorizationStartError")<{
5050
readonly cause: unknown;
51+
readonly message: string;
5152
}> {}
5253

5354
export type StartOAuthAuthorizationInput<TPayload extends OAuthCompletionPayload> = {
@@ -153,11 +154,15 @@ export function useOAuthPopupFlow<
153154
const startExit = await Effect.runPromiseExit(
154155
Effect.tryPromise({
155156
try: input.run,
156-
catch: (cause) => new OAuthAuthorizationStartError({ cause }),
157+
catch: (cause) =>
158+
new OAuthAuthorizationStartError({
159+
cause,
160+
message: messageFromUnknown(cause, startErrorMessage ?? "Failed to start sign-in"),
161+
}),
157162
}),
158163
);
159164
if (Exit.isFailure(startExit)) {
160-
const message = startErrorMessage ?? "Failed to start sign-in";
165+
const message = messageFromExit(startExit, startErrorMessage ?? "Failed to start sign-in");
161166
reportHandledError(startExit.cause, {
162167
surface: "oauth",
163168
action: "start",
@@ -278,7 +283,11 @@ export function useOAuthPopupFlow<
278283
}).then((exit) =>
279284
Exit.isSuccess(exit)
280285
? exit.value
281-
: Effect.runPromise(Effect.fail(startErrorMessage ?? "Failed to start sign-in")),
286+
: Effect.runPromise(
287+
Effect.fail({
288+
message: messageFromExit(exit, startErrorMessage ?? "Failed to start sign-in"),
289+
}),
290+
),
282291
),
283292
});
284293
},

0 commit comments

Comments
 (0)