Skip to content

Commit 080eef1

Browse files
feat(oauth-proxy): add Deco Store OAuth helpers and MCP detection (#2129)
* feat(oauth-proxy): add Deco Store OAuth helpers and MCP detection - Implemented `getDecoStoreProjectLocator` to retrieve project locators from the Deco Store registry. - Added `buildDecoOAuthParams` to construct OAuth query parameters for deco-hosted MCPs. - Enhanced the main app logic to include smart OAuth parameters for deco-hosted MCPs. - Introduced `isDecoHostedMcp` function to identify deco-hosted MCP URLs. - Updated tests to validate the new functionality and ensure correct behavior for deco-hosted MCP detection. * fix * rm log * handle possible error
1 parent d2a6bdd commit 080eef1

3 files changed

Lines changed: 139 additions & 1 deletion

File tree

apps/mesh/src/api/app.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,59 @@ import oauthProxyRoutes, {
3030
fetchProtectedResourceMetadata,
3131
} from "./routes/oauth-proxy";
3232
import proxyRoutes from "./routes/proxy";
33+
import { isDecoHostedMcp, DECO_STORE_URL } from "../core/well-known-mcp";
34+
import type { MeshContext } from "../core/mesh-context";
3335

3436
// Track current event bus instance for cleanup during HMR
3537
let currentEventBus: EventBus | null = null;
3638

39+
// ============================================================================
40+
// Deco Store OAuth Helpers
41+
// ============================================================================
42+
43+
/**
44+
* Get project_locator from the Deco Store registry connection.
45+
* Returns the locator string or null if not found/configured.
46+
*
47+
* @param ctx - The mesh context
48+
* @param organizationId - The organization ID to search for the registry connection
49+
*/
50+
async function getDecoStoreProjectLocator(
51+
ctx: MeshContext,
52+
organizationId: string,
53+
): Promise<string | null> {
54+
// Find registry connection by URL within the organization
55+
const connections = await ctx.storage.connections.list(organizationId);
56+
const registryConn = connections.find((c) =>
57+
c.connection_url?.startsWith(DECO_STORE_URL),
58+
);
59+
60+
if (!registryConn?.configuration_state) {
61+
return null;
62+
}
63+
64+
return (registryConn.configuration_state as Record<string, unknown>)
65+
.project_locator as string | null;
66+
}
67+
68+
/**
69+
* Build OAuth query params for deco-hosted MCPs.
70+
* Uses project_locator from Deco Store registry or falls back to auto_personal.
71+
*/
72+
function buildDecoOAuthParams(projectLocator: string | null): URLSearchParams {
73+
const params = new URLSearchParams();
74+
75+
if (projectLocator) {
76+
const [org, project] = projectLocator.split("/");
77+
if (org) params.set("workspace_hint", org);
78+
if (project) params.set("project_hint", project);
79+
} else {
80+
params.set("auto_personal", "true");
81+
}
82+
83+
return params;
84+
}
85+
3786
// Create serializer for Prometheus text format (shared across instances)
3887
const prometheusSerializer = new PrometheusSerializer();
3988

@@ -281,6 +330,27 @@ export function createApp(options: CreateAppOptions = {}) {
281330
if (targetUrl.searchParams.has("resource")) {
282331
targetUrl.searchParams.set("resource", connection.connection_url);
283332
}
333+
334+
// Add smart OAuth params for deco-hosted MCPs to skip org/project selection
335+
// Wrapped in try-catch to ensure OAuth redirect proceeds even if smart params fail
336+
if (isDecoHostedMcp(connection.connection_url)) {
337+
try {
338+
const projectLocator = await getDecoStoreProjectLocator(
339+
ctx,
340+
connection.organization_id,
341+
);
342+
const smartParams = buildDecoOAuthParams(projectLocator);
343+
for (const [key, value] of smartParams) {
344+
targetUrl.searchParams.set(key, value);
345+
}
346+
} catch (error) {
347+
console.warn(
348+
"[oauth-proxy] Failed to get smart OAuth params, proceeding without:",
349+
error,
350+
);
351+
}
352+
}
353+
284354
return c.redirect(targetUrl.toString(), 302);
285355
}
286356

apps/mesh/src/api/routes/oauth-proxy.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ import {
1010
import { Hono } from "hono";
1111
import oauthProxyRoutes from "./oauth-proxy";
1212
import { ContextFactory } from "../../core/context-factory";
13+
import {
14+
isDecoHostedMcp,
15+
DECO_STORE_URL,
16+
DECO_CMS_API_HOST,
17+
} from "../../core/well-known-mcp";
1318

1419
describe("OAuth Proxy Routes", () => {
1520
let app: Hono;
@@ -660,3 +665,46 @@ describe("OAuth URL Path Construction", () => {
660665
);
661666
});
662667
});
668+
669+
describe("Deco-Hosted MCP Detection", () => {
670+
test("DECO_CMS_API_HOST is correct", () => {
671+
expect(DECO_CMS_API_HOST).toBe("api.decocms.com");
672+
});
673+
674+
test("DECO_STORE_URL is correct", () => {
675+
expect(DECO_STORE_URL).toBe("https://api.decocms.com/mcp/registry");
676+
});
677+
678+
describe("isDecoHostedMcp", () => {
679+
test("returns true for deco-hosted MCP URLs", () => {
680+
expect(
681+
isDecoHostedMcp("https://api.decocms.com/apps/deco/github/mcp"),
682+
).toBe(true);
683+
expect(isDecoHostedMcp("https://api.decocms.com/mcp/some-app")).toBe(
684+
true,
685+
);
686+
expect(isDecoHostedMcp("https://api.decocms.com/apps/xyz/abc/mcp")).toBe(
687+
true,
688+
);
689+
});
690+
691+
test("returns false for Deco Store registry (public, no OAuth)", () => {
692+
expect(isDecoHostedMcp(DECO_STORE_URL)).toBe(false);
693+
expect(isDecoHostedMcp("https://api.decocms.com/mcp/registry")).toBe(
694+
false,
695+
);
696+
});
697+
698+
test("returns false for non-deco-hosted URLs", () => {
699+
expect(isDecoHostedMcp("https://stripe.mcp.run/mcp")).toBe(false);
700+
expect(isDecoHostedMcp("https://mcp.example.com/api")).toBe(false);
701+
expect(isDecoHostedMcp("https://other.decocms.com/mcp")).toBe(false);
702+
});
703+
704+
test("returns false for null or invalid URLs", () => {
705+
expect(isDecoHostedMcp(null)).toBe(false);
706+
expect(isDecoHostedMcp("")).toBe(false);
707+
expect(isDecoHostedMcp("not-a-url")).toBe(false);
708+
});
709+
});
710+
});

apps/mesh/src/core/well-known-mcp.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
import type { ConnectionCreateData } from "@/tools/connection/schema";
22

3+
/** Deco CMS API host for detecting deco-hosted MCPs */
4+
export const DECO_CMS_API_HOST = "api.decocms.com";
5+
6+
/** The Deco Store registry URL (public, no OAuth) */
7+
export const DECO_STORE_URL = "https://api.decocms.com/mcp/registry";
8+
9+
/**
10+
* Check if a connection URL is a deco-hosted MCP (excluding the registry itself).
11+
* Used to determine if smart OAuth params should be added.
12+
*/
13+
export function isDecoHostedMcp(connectionUrl: string | null): boolean {
14+
if (!connectionUrl) return false;
15+
try {
16+
const url = new URL(connectionUrl);
17+
return url.host === DECO_CMS_API_HOST && connectionUrl !== DECO_STORE_URL;
18+
} catch {
19+
return false;
20+
}
21+
}
22+
323
export const WellKnownMCPId = {
424
SELF: "self",
525
REGISTRY: "registry",
@@ -26,7 +46,7 @@ export function getWellKnownRegistryConnection(
2646
title: "Deco Store",
2747
description: "Official deco MCP registry with curated integrations",
2848
connection_type: "HTTP",
29-
connection_url: "https://api.decocms.com/mcp/registry",
49+
connection_url: DECO_STORE_URL,
3050
icon: "https://assets.decocache.com/decocms/00ccf6c3-9e13-4517-83b0-75ab84554bb9/596364c63320075ca58483660156b6d9de9b526e.png",
3151
app_name: "deco-registry",
3252
app_id: null,

0 commit comments

Comments
 (0)