Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 100 additions & 5 deletions packages/react/src/components/add-account-modal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ describe("runDcrConnect", () => {
discoveryUrl: "https://mcp.example.com/mcp",
owner: "user",
integrationName: "Linear MCP",
existingSlugs: [],
existingClients: [],
redirectUri: "https://localhost:5394/api/oauth/callback",
integration: TEST_INTEGRATION,
},
Expand All @@ -259,6 +259,101 @@ describe("runDcrConnect", () => {
expect(startArgs!.owner).toBe("user");
});

it("reuses an existing client minted for this integration instead of minting a duplicate", async () => {
// Regression guard for "Datadog, Datadog 2, … Datadog 10": a prior DCR client
// for THIS integration (same owner, same token host) must be reused, so a
// second connect runs only `start` — never `register`.
const calls: string[] = [];
let startArgs: StartArgs | null = null;
const outcome = await runDcrConnect(
{
probe: (): Promise<ProbeResult | null> => {
calls.push("probe");
return Promise.resolve({
authorizationUrl: "https://auth.example.com/authorize",
tokenUrl: "https://auth.example.com/token",
registrationEndpoint: "https://auth.example.com/register",
});
},
register: (args: RegisterArgs): Promise<OAuthClientSlug | null> => {
calls.push("register");
return Promise.resolve(args.slug);
},
start: (args: StartArgs): void => {
calls.push("start");
startArgs = args;
},
},
{
discoveryUrl: "https://mcp.example.com/mcp",
owner: "user",
integrationName: "Linear MCP",
existingClients: [
{
owner: "user",
slug: OAuthClientSlug.make("linear-mcp"),
grant: "authorization_code",
authorizationUrl: "https://auth.example.com/authorize",
tokenUrl: "https://auth.example.com/token",
clientId: "minted-earlier",
origin: { kind: "dynamic_client_registration", integration: TEST_INTEGRATION },
},
],
integration: TEST_INTEGRATION,
},
);

expect(outcome.kind).toBe("started");
// Probed (to confirm the same server), then reused — NO second registration.
expect(calls).toEqual(["probe", "start"]);
expect(String(startArgs!.client)).toBe("linear-mcp");
});

it("does not reuse a client belonging to a DIFFERENT integration", async () => {
// A Datadog DCR client must never be reused for an Atlassian connect, even
// though both went through DCR — provenance is keyed by integration.
const calls: string[] = [];
await runDcrConnect(
{
probe: (): Promise<ProbeResult | null> =>
Promise.resolve({
authorizationUrl: "https://auth.atlassian.com/authorize",
tokenUrl: "https://auth.atlassian.com/token",
registrationEndpoint: "https://auth.atlassian.com/register",
}),
register: (args: RegisterArgs): Promise<OAuthClientSlug | null> => {
calls.push("register");
return Promise.resolve(args.slug);
},
start: (): void => {
calls.push("start");
},
},
{
discoveryUrl: "https://mcp.atlassian.com/mcp",
owner: "user",
integrationName: "Atlassian",
existingClients: [
{
owner: "user",
slug: OAuthClientSlug.make("datadog"),
grant: "authorization_code",
authorizationUrl: "https://app.datadoghq.com/oauth2/authorize",
tokenUrl: "https://api.datadoghq.com/oauth2/token",
clientId: "datadog-client",
origin: {
kind: "dynamic_client_registration",
integration: IntegrationSlug.make("datadog"),
},
},
],
integration: TEST_INTEGRATION,
},
);
// No Atlassian client exists yet → it mints a fresh one, not reuse Datadog.
expect(calls).toContain("register");
});

it("prefers declared scopes over probed scopes when present", async () => {
let registerArgs: RegisterArgs | null = null;
const outcome = await runDcrConnect(
Expand All @@ -280,7 +375,7 @@ describe("runDcrConnect", () => {
discoveryUrl: "https://mcp.example.com/mcp",
owner: "user",
integrationName: "App",
existingSlugs: [],
existingClients: [],
declaredScopes: ["declared.scope"],
integration: TEST_INTEGRATION,
},
Expand Down Expand Up @@ -313,7 +408,7 @@ describe("runDcrConnect", () => {
discoveryUrl: "https://mcp.example.com/mcp",
owner: "user",
integrationName: "App",
existingSlugs: [],
existingClients: [],
integration: TEST_INTEGRATION,
},
);
Expand Down Expand Up @@ -344,7 +439,7 @@ describe("runDcrConnect", () => {
discoveryUrl: "https://mcp.example.com/mcp",
owner: "user",
integrationName: "App",
existingSlugs: [],
existingClients: [],
integration: TEST_INTEGRATION,
},
);
Expand Down Expand Up @@ -374,7 +469,7 @@ describe("runDcrConnect", () => {
discoveryUrl: "https://mcp.example.com/mcp",
owner: "user",
integrationName: "App",
existingSlugs: [],
existingClients: [],
integration: TEST_INTEGRATION,
},
);
Expand Down
34 changes: 28 additions & 6 deletions packages/react/src/components/add-account-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { oauthCallbackUrl, useOAuthPopupFlow } from "../plugins/oauth-sign-in";
import {
clientDisplayName,
clientHost,
getRootDomain,
uniqueClientSlug,
useOAuthClientsForIntegration,
type OAuthClientOption,
Expand Down Expand Up @@ -471,8 +472,10 @@ type RunDcrConnectInput = {
readonly discoveryUrl: string;
readonly owner: Owner;
readonly integrationName: string;
/** The owner's existing client slugs, so the minted slug stays unique. */
readonly existingSlugs: readonly string[];
/** The owner's existing apps (UNFILTERED). Used both to keep a minted slug
* unique across all apps AND to reuse a client already minted for this
* integration instead of minting a duplicate on every connect. */
readonly existingClients: readonly OAuthClientOption[];
/** Scopes declared by the integration's method (override the probed ones). */
readonly declaredScopes?: readonly string[];
/** Browser-facing callback URL registered with DCR when available. */
Expand Down Expand Up @@ -504,7 +507,28 @@ export async function runDcrConnect(
const registrationEndpoint = probe.registrationEndpoint;
if (!registrationEndpoint) return { kind: "fallback", reason: "no-registration-endpoint" };

const slug = uniqueClientSlug(input.integrationName, input.existingSlugs);
// Reuse a client already minted for THIS integration (same owner, same
// discovered token host) instead of minting a duplicate every connect — the
// cause of "Datadog", "Datadog 2", … "Datadog 10" piling up. A fresh probe of
// the same server yields the same endpoints, so an existing DCR client is
// still valid; we re-run only the authorize/token flow with it.
const probedTokenRoot = getRootDomain(probe.tokenUrl);
const reusable = input.existingClients.find(
(app) =>
app.owner === input.owner &&
app.origin?.integration != null &&
String(app.origin.integration) === String(input.integration) &&
getRootDomain(app.tokenUrl) === probedTokenRoot,
);
Comment on lines +515 to +522

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 getRootDomain returns string | undefined. When probe.tokenUrl is unparseable, probedTokenRoot is undefined, and any existing client whose tokenUrl is also unparseable will satisfy undefined === undefined, producing a spurious match. Add an explicit guard so that an undefined domain never matches anything.

Suggested change
const probedTokenRoot = getRootDomain(probe.tokenUrl);
const reusable = input.existingClients.find(
(app) =>
app.owner === input.owner &&
app.origin?.integration != null &&
String(app.origin.integration) === String(input.integration) &&
getRootDomain(app.tokenUrl) === probedTokenRoot,
);
const probedTokenRoot = getRootDomain(probe.tokenUrl);
const reusable =
probedTokenRoot != null
? input.existingClients.find(
(app) =>
app.owner === input.owner &&
app.origin?.integration != null &&
String(app.origin.integration) === String(input.integration) &&
getRootDomain(app.tokenUrl) === probedTokenRoot,
)
: undefined;

if (reusable) {
deps.start({ client: reusable.slug, owner: input.owner });
return { kind: "started" };
}
Comment on lines 507 to +526

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Reuse check unreachable when server stops advertising its DCR endpoint

The if (!registrationEndpoint) return { kind: "fallback", reason: "no-registration-endpoint" } guard fires at line 508, before the reusable-client search is reached. If a server probe succeeds but the server temporarily (or permanently) drops its registration_endpoint from discovery metadata, any existing valid DCR client for that integration is ignored and the user hits "Automatic setup unavailable". The reuse path doesn't use registrationEndpoint at all — it only needs probe.tokenUrl — so the check should run after the reuse attempt, or the two paths should be split: try reuse first, only then require registrationEndpoint before minting.


const slug = uniqueClientSlug(
input.integrationName,
input.existingClients.map((app) => String(app.slug)),
);
const scopes =
input.declaredScopes && input.declaredScopes.length > 0
? input.declaredScopes
Expand Down Expand Up @@ -1145,9 +1169,7 @@ function AddAccountModalView(props: AddAccountModalProps) {
discoveryUrl,
owner: dcrOwner,
integrationName,
existingSlugs: [...oauthApps, ...oauthOtherApps].map((app: OAuthClientOption) =>
String(app.slug),
),
existingClients: oauthAllClients,
declaredScopes: method.oauth?.scopes,
redirectUri: oauthCallbackUrl(),
integration,
Expand Down
Loading