buildmymcpserver/apps/web/lib/install-snippets.ts
Marco Sadjadi 3a05766f88
All checks were successful
Deploy to Production / deploy (push) Successful in 1m28s
fix(oauth): allow generic RFC 7591 DCR + expand install snippets
- /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.
2026-05-28 17:20:01 +02:00

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.',
};
}
}