All checks were successful
Deploy to Production / deploy (push) Successful in 1m28s
- /oauth/register: drop resource_required check, accept generic registrations (Claude Desktop omits resource in DCR body per spec). serverId stored as NULL; /authorize still enforces org-ownership + access-token aud claim still pinned to resource. Fixes Claude Desktop DCR failure (ofid_d7e39530c109fa7f). - /oauth/authorize: skip strict server.id check when client.serverId is NULL (generic client); org check remains the security boundary. - schema: oauth_clients.server_id no longer NOT NULL. - migration 0002: ALTER COLUMN server_id DROP NOT NULL (already applied on prod). - install-snippets: add Claude Code (CLI), VS Code, Codex, raw URL tabs. Claude Desktop now shows form-field values (Name / Remote MCP Server URL / OAuth Client ID / Secret) matching the new Custom Connector UI instead of the obsolete JSON config. - types: InstallTarget enum extended. - hero-video: clicking the audio toggle restarts the video from frame 0 so unmute aligns with the spoken opening. - marketing: drop em-dashes from rendered copy.
144 lines
4.7 KiB
TypeScript
144 lines
4.7 KiB
TypeScript
import type { InstallTarget } from '@bmm/types';
|
|
|
|
export interface SnippetInput {
|
|
name: string;
|
|
slug: string;
|
|
publicUrl: string;
|
|
/**
|
|
* Optional pre-issued OAuth client credentials. When present, Claude
|
|
* Desktop / ChatGPT users can paste them into the "OAuth Client ID /
|
|
* Secret" form fields as a fallback if Dynamic Client Registration
|
|
* (DCR) is blocked by their network or the client refuses it.
|
|
*/
|
|
oauthClientId?: string;
|
|
oauthClientSecret?: string;
|
|
}
|
|
|
|
export interface SnippetOutput {
|
|
label: string;
|
|
code: string;
|
|
/** Optional short hint shown below the snippet. */
|
|
note?: string;
|
|
}
|
|
|
|
const stripTrailingSlash = (u: string) => u.replace(/\/$/, '');
|
|
|
|
export function buildSnippet(target: InstallTarget, input: SnippetInput): SnippetOutput {
|
|
const key = input.slug.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
const baseUrl = stripTrailingSlash(input.publicUrl);
|
|
const mcpUrl = `${baseUrl}/mcp`;
|
|
const hasCreds = !!(input.oauthClientId && input.oauthClientSecret);
|
|
|
|
switch (target) {
|
|
case 'claude-desktop': {
|
|
// Claude Desktop now uses a "Custom Connector" UI with form fields:
|
|
// Name / Remote MCP Server URL / OAuth Client ID / OAuth Client Secret.
|
|
// We show the exact values to paste so the JSON-config dance is gone.
|
|
// OAuth fields are blank by default (DCR registers them automatically)
|
|
// and only filled when the org has pre-issued credentials as a fallback.
|
|
const lines = [
|
|
`Name: ${input.name}`,
|
|
`Remote MCP Server URL: ${mcpUrl}`,
|
|
];
|
|
if (hasCreds) {
|
|
lines.push(
|
|
`OAuth Client ID: ${input.oauthClientId}`,
|
|
`OAuth Client Secret: ${input.oauthClientSecret}`,
|
|
);
|
|
} else {
|
|
lines.push(
|
|
`OAuth Client ID: (leave blank, registers automatically)`,
|
|
`OAuth Client Secret: (leave blank, registers automatically)`,
|
|
);
|
|
}
|
|
return {
|
|
label: 'Claude Desktop · Custom Connector',
|
|
code: lines.join('\n'),
|
|
note: hasCreds
|
|
? 'Settings → Connectors → Add custom connector. Paste each value into the matching field. OAuth handshake runs on first use.'
|
|
: 'Settings → Connectors → Add custom connector. Paste Name + URL, leave the OAuth fields blank. If your network blocks Dynamic Client Registration, generate a fixed Client ID / Secret in the server settings and paste them too.',
|
|
};
|
|
}
|
|
|
|
case 'claude-code':
|
|
return {
|
|
label: 'Claude Code (CLI)',
|
|
code: `claude mcp add ${key} ${mcpUrl} --transport http`,
|
|
note: `Run in any project. Use \`claude mcp list\` to confirm and \`claude mcp remove ${key}\` to undo.`,
|
|
};
|
|
|
|
case 'cursor':
|
|
return {
|
|
label: '.cursor/mcp.json',
|
|
code: JSON.stringify(
|
|
{
|
|
mcpServers: {
|
|
[key]: {
|
|
url: mcpUrl,
|
|
auth: 'oauth2',
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
note: 'Place at the project root or in ~/.cursor/mcp.json for global use. Restart Cursor after saving.',
|
|
};
|
|
|
|
case 'vscode':
|
|
return {
|
|
label: '.vscode/mcp.json',
|
|
code: JSON.stringify(
|
|
{
|
|
servers: {
|
|
[key]: {
|
|
type: 'http',
|
|
url: mcpUrl,
|
|
},
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
note: 'VS Code → Copilot Chat → MCP servers. Save the file at the workspace root, then reload the window. OAuth runs on first call.',
|
|
};
|
|
|
|
case 'chatgpt': {
|
|
const lines = [
|
|
`Name: ${input.name}`,
|
|
`URL: ${mcpUrl}`,
|
|
`Auth: OAuth 2.1`,
|
|
];
|
|
if (hasCreds) {
|
|
lines.push(
|
|
`Client ID: ${input.oauthClientId}`,
|
|
`Client Secret: ${input.oauthClientSecret}`,
|
|
);
|
|
}
|
|
return {
|
|
label: 'ChatGPT · Custom Connector',
|
|
code: lines.join('\n'),
|
|
note: hasCreds
|
|
? 'ChatGPT → Settings → Connectors → Add custom connector. Paste the values above.'
|
|
: 'ChatGPT → Settings → Connectors → Add custom connector. Paste Name + URL. OAuth handshake registers a client automatically on first use.',
|
|
};
|
|
}
|
|
|
|
case 'codex':
|
|
return {
|
|
label: '~/.codex/config.toml',
|
|
code: `[mcp_servers.${key}]
|
|
type = "http"
|
|
url = "${mcpUrl}"`,
|
|
note: 'Append to your ~/.codex/config.toml. Restart the Codex CLI. OAuth runs on first tool call.',
|
|
};
|
|
|
|
case 'raw-url':
|
|
return {
|
|
label: 'Server URL',
|
|
code: mcpUrl,
|
|
note: 'Any MCP-compatible client. Add it as a "Remote MCP server" (HTTP transport) and approve OAuth on first use.',
|
|
};
|
|
}
|
|
}
|