feat(connect): dedicated Claude.ai / ChatGPT browser connect picker

- Replace single 'Connect to Claude / ChatGPT' button with a modal picker
  offering Claude.ai web, Claude Desktop, Codex CLI, and ChatGPT/GPT Actions.
- Add /oauth/connect-claude-ai backend route that redirects to Anthropic's
  official https://claude.ai/api/mcp/auth_callback OAuth callback.
- Update MCP callback result page with browser-specific instructions for
  Claude.ai web, Claude Desktop, ChatGPT/GPT Actions, and Codex CLI.
- Deploy new app and hermes images to K8s.
This commit is contained in:
Garfield
2026-06-12 14:55:36 -04:00
parent 51315527c0
commit 6604ab5d2b
6 changed files with 180 additions and 14 deletions

View File

@@ -22,7 +22,7 @@ spec:
fsGroup: 1000 fsGroup: 1000
containers: containers:
- name: hermes-mcp - name: hermes-mcp
image: localhost:32000/hermes-mcp@sha256:d282f50a2409b322ffbf5a7e10511493e3fcf7346a9fb47ecfac10b8cbe02660 image: localhost:32000/hermes-mcp@sha256:e5dec7b23a64f2f2d7977d24ca789dbcc077bcffb86a5b623d6ec64ff2436e06
imagePullPolicy: Always imagePullPolicy: Always
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
@@ -158,11 +158,11 @@ spec:
- name: PILOT_CUSTOMER_ID - name: PILOT_CUSTOMER_ID
value: "9a3f1a23-3080-4f9f-932c-02dae813ee96" value: "9a3f1a23-3080-4f9f-932c-02dae813ee96"
- name: FACEBOOK_DEFAULT_ACCESS_TOKEN - name: FACEBOOK_DEFAULT_ACCESS_TOKEN
value: "EAAYG3FLDWzMBRgOmCM5GX7E3L6zk5utoZCn9eZAVvk0Ein6NaYtDZCtD5aMP3yMDnB0X2EoqvIYeOU77PhCCNaCve9LwX8iyQ2UsxsCajeHc7SXQL4EYWB7UEsDbcRA2gRF8GITYgbhBKKRlE3ehlwWBySwfxVexzMDgkGgz3ctzK4144hgJnE3LZB8EHP2FvolqNpXPVitexunWN0hxRwVXUSDgZCiOfzXfa1t0smxDs5wZDZD" value: "EAAYG3FLDWzMBRmZBDhn1rePtuKDCLUkzHLyJHNJA7yXXdcNUPXmyZA36BwLp7vXHhOxguCIGZB3JfJIhgX2ZBRZBTmZCDfdAYeZBrFAye2L5cIUKvYdjYYA3mlT3ZAacEQgmbhYuKBp4eCOQz0rrNUwLZB2qspvO9wczZAM3tWqFctYBP10oGfgOJIQ8ITweRU2Bgdte2hod66"
- name: FACEBOOK_DEFAULT_PAGE_ID - name: FACEBOOK_DEFAULT_PAGE_ID
value: "1152192567968569" value: "1152192567968569"
- name: INSTAGRAM_DEFAULT_ACCESS_TOKEN - name: INSTAGRAM_DEFAULT_ACCESS_TOKEN
value: "EAAYG3FLDWzMBRgOmCM5GX7E3L6zk5utoZCn9eZAVvk0Ein6NaYtDZCtD5aMP3yMDnB0X2EoqvIYeOU77PhCCNaCve9LwX8iyQ2UsxsCajeHc7SXQL4EYWB7UEsDbcRA2gRF8GITYgbhBKKRlE3ehlwWBySwfxVexzMDgkGgz3ctzK4144hgJnE3LZB8EHP2FvolqNpXPVitexunWN0hxRwVXUSDgZCiOfzXfa1t0smxDs5wZDZD" value: "EAAYG3FLDWzMBRmZBDhn1rePtuKDCLUkzHLyJHNJA7yXXdcNUPXmyZA36BwLp7vXHhOxguCIGZB3JfJIhgX2ZBRZBTmZCDfdAYeZBrFAye2L5cIUKvYdjYYA3mlT3ZAacEQgmbhYuKBp4eCOQz0rrNUwLZB2qspvO9wczZAM3tWqFctYBP10oGfgOJIQ8ITweRU2Bgdte2hod66"
- name: INSTAGRAM_DEFAULT_BUSINESS_ACCOUNT_ID - name: INSTAGRAM_DEFAULT_BUSINESS_ACCOUNT_ID
value: "17841422623735880" value: "17841422623735880"
- name: WHATSAPP_DEFAULT_ACCESS_TOKEN - name: WHATSAPP_DEFAULT_ACCESS_TOKEN

View File

@@ -15,7 +15,7 @@ spec:
spec: spec:
containers: containers:
- name: squaremcp-app - name: squaremcp-app
image: localhost:32000/squaremcp-app@sha256:88c9163b49a881b2d8e59f46d66297902109026d138e9562b9fcc1bb0f4ab4d6 image: localhost:32000/squaremcp-app@sha256:9c2601dd74bfca9f22350a38dc616eb8a76580090587803911bb2e5633ace361
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8080 - containerPort: 8080

View File

@@ -252,9 +252,71 @@ logoutBtn.addEventListener('click', async () => {
showLogin(); showLogin();
}); });
// Connect MCP Client — start the browser OAuth flow // Connect MCP Client — show picker for Claude.ai / ChatGPT / desktop / CLI
document.getElementById('connect-mcp-btn')?.addEventListener('click', () => { document.getElementById('connect-mcp-btn')?.addEventListener('click', () => {
window.open(`${API_BASE}/oauth/connect-mcp`, '_blank', 'width=560,height=600,noopener'); openModal(renderMcpClientPicker());
});
function renderMcpClientPicker() {
return `
<div class="mcp-picker">
<h3>Connect an AI client</h3>
<p class="picker-subtitle">Choose where you want to use SquareMCP tools.</p>
<div class="picker-option">
<div class="picker-meta">
<div class="picker-title">Claude.ai (web)</div>
<div class="picker-desc">Use SquareMCP directly in your browser at claude.ai.</div>
</div>
<a class="btn btn-primary" href="${API_BASE}/oauth/connect-claude-ai" target="_blank" rel="noopener" onclick="window.closeMcpPicker && window.closeMcpPicker()">Connect</a>
</div>
<div class="picker-option">
<div class="picker-meta">
<div class="picker-title">Claude Desktop</div>
<div class="picker-desc">macOS / Windows app with local MCP config.</div>
</div>
<button class="btn btn-primary" data-connect="claude-desktop">Connect</button>
</div>
<div class="picker-option">
<div class="picker-meta">
<div class="picker-title">Codex CLI / OpenCode</div>
<div class="picker-desc">Terminal-based agents (OpenAI, opencode).</div>
</div>
<button class="btn btn-primary" data-connect="codex">Connect</button>
</div>
<div class="picker-option">
<div class="picker-meta">
<div class="picker-title">ChatGPT (web)</div>
<div class="picker-desc">Copy the OpenAPI spec URL for GPT Actions.</div>
</div>
<button class="btn btn-secondary" data-connect="chatgpt">Get URL</button>
</div>
</div>
`;
}
window.closeMcpPicker = closeModal;
modalBody.addEventListener('click', (e) => {
const btn = e.target.closest('[data-connect]');
if (!btn) return;
const type = btn.dataset.connect;
if (type === 'claude-desktop' || type === 'codex') {
window.open(`${API_BASE}/oauth/connect-mcp`, '_blank', 'width=560,height=600,noopener');
} else if (type === 'chatgpt') {
openModal(`
<div class="mcp-picker">
<h3>ChatGPT / GPT Actions</h3>
<p class="picker-subtitle">ChatGPT browser does not yet support native MCP. Use this OpenAPI spec URL in a GPT Action:</p>
<div class="token-box" style="margin:16px 0;">${API_BASE}/openapi.json</div>
<p class="picker-subtitle">Set authentication to <strong>Bearer token</strong> and paste your API key from <em>Settings → API Keys</em>.</p>
<button class="btn btn-primary" onclick="window.open('${API_BASE}/openapi.json','_blank')">Open spec</button>
</div>
`);
}
}); });
// Password reset request // Password reset request

View File

@@ -94,7 +94,7 @@
<section class="welcome"> <section class="welcome">
<h2>Connect your accounts</h2> <h2>Connect your accounts</h2>
<p>Connect once. Then ask Claude or ChatGPT to post, search your notes, or send email — without touching any of these apps.</p> <p>Connect once. Then ask Claude or ChatGPT to post, search your notes, or send email — without touching any of these apps.</p>
<button id="connect-mcp-btn" class="btn btn-primary" style="margin-top:16px;">Connect to Claude / ChatGPT</button> <button id="connect-mcp-btn" class="btn btn-primary" style="margin-top:16px;" aria-label="Connect Claude.ai, ChatGPT, Claude Desktop, or Codex CLI" title="Connect Claude.ai, ChatGPT, Claude Desktop, or Codex CLI">Connect AI Client</button>
</section> </section>
<section class="usage-bar" id="usage-bar"> <section class="usage-bar" id="usage-bar">

View File

@@ -751,3 +751,66 @@ body {
text-align: center; text-align: center;
min-height: 18px; min-height: 18px;
} }
/* MCP client picker modal */
.mcp-picker h3 {
margin: 0 0 6px;
font-size: 1.25rem;
}
.picker-subtitle {
color: var(--text-secondary);
font-size: 0.9rem;
margin: 0 0 20px;
line-height: 1.5;
}
.picker-option {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 16px;
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 12px;
background: var(--background);
}
.picker-option:last-child {
margin-bottom: 0;
}
.picker-meta {
flex: 1;
min-width: 0;
}
.picker-title {
font-weight: 600;
font-size: 0.95rem;
margin-bottom: 4px;
}
.picker-desc {
color: var(--text-secondary);
font-size: 0.8rem;
line-height: 1.4;
}
.picker-option .btn {
white-space: nowrap;
padding: 8px 14px;
font-size: 0.85rem;
}
.token-box {
background: var(--background);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 12px;
font-family: 'SF Mono', monospace;
font-size: 0.85rem;
word-break: break-all;
}

View File

@@ -724,6 +724,23 @@ app.get('/oauth/connect-mcp', (req, res) => {
res.redirect(`/oauth/authorize?${params}`); res.redirect(`/oauth/authorize?${params}`);
}); });
// Dedicated entry point for the Claude.ai web MCP client. It uses the official
// Anthropic redirect_uri so Claude.ai receives the authorization code directly.
app.get('/oauth/connect-claude-ai', (req, res) => {
const clientId = process.env.OAUTH_CLIENT_ID;
if (!clientId) {
res.status(503).send('MCP OAuth app not configured (OAUTH_CLIENT_ID missing)');
return;
}
const params = new URLSearchParams({
client_id: clientId,
redirect_uri: 'https://claude.ai/api/mcp/auth_callback',
response_type: 'code',
scope: 'mcp',
});
res.redirect(`/oauth/authorize?${params}`);
});
// Callback — exchange code for token and render the config snippet page // Callback — exchange code for token and render the config snippet page
app.get('/oauth/mcp-callback', async (req, res) => { app.get('/oauth/mcp-callback', async (req, res) => {
const code = req.query.code as string | undefined; const code = req.query.code as string | undefined;
@@ -762,11 +779,12 @@ h1{color:#dc2626;margin:0 0 12px}p{color:#888;margin:0}</style></head>
} }
const { token, serverUrl } = opts; const { token, serverUrl } = opts;
const mcpUrl = `${serverUrl}/mcp`;
const claudeConfig = JSON.stringify({ const claudeConfig = JSON.stringify({
mcpServers: { 'hermes-mcp': { type: 'http', url: `${serverUrl}/mcp`, headers: { Authorization: `Bearer ${token}` } } } mcpServers: { 'hermes-mcp': { type: 'http', url: mcpUrl, headers: { Authorization: `Bearer ${token}` } } }
}, null, 2); }, null, 2);
const codexConfig = JSON.stringify({ const codexConfig = JSON.stringify({
mcpServers: { 'hermes-mcp': { type: 'http', url: `${serverUrl}/mcp`, headers: { Authorization: `Bearer ${token}` } } } mcpServers: { 'hermes-mcp': { type: 'http', url: mcpUrl, headers: { Authorization: `Bearer ${token}` } } }
}, null, 2); }, null, 2);
const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;'); const esc = (s: string) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
@@ -781,12 +799,18 @@ body{font-family:system-ui,sans-serif;background:#0f0f10;color:#e5e5e5;margin:0;
.card{background:#1a1a1b;border:1px solid #2a2a2b;border-radius:12px;padding:32px;max-width:680px;margin:0 auto} .card{background:#1a1a1b;border:1px solid #2a2a2b;border-radius:12px;padding:32px;max-width:680px;margin:0 auto}
h1{font-size:22px;margin:0 0 8px;color:#10a37f} h1{font-size:22px;margin:0 0 8px;color:#10a37f}
.subtitle{color:#888;margin:0 0 28px;font-size:14px} .subtitle{color:#888;margin:0 0 28px;font-size:14px}
h2{font-size:14px;font-weight:600;color:#888;text-transform:uppercase;letter-spacing:.05em;margin:20px 0 8px} h2{font-size:14px;font-weight:600;color:#888;text-transform:uppercase;letter-spacing:.05em;margin:24px 0 10px}
pre{background:#0f0f10;border:1px solid #2a2a2b;border-radius:8px;padding:16px;font-size:12px;overflow-x:auto;position:relative} pre{background:#0f0f10;border:1px solid #2a2a2b;border-radius:8px;padding:16px;font-size:12px;overflow-x:auto;position:relative}
.copy-btn{position:absolute;top:8px;right:8px;background:#2a2a2b;border:none;color:#888;padding:4px 10px;border-radius:6px;cursor:pointer;font-size:11px} .copy-btn{position:absolute;top:8px;right:8px;background:#2a2a2b;border:none;color:#888;padding:4px 10px;border-radius:6px;cursor:pointer;font-size:11px}
.copy-btn:hover{color:#e5e5e5} .copy-btn:hover{color:#e5e5e5}
.token-box{background:#0f0f10;border:1px solid #2a2a2b;border-radius:8px;padding:12px 16px;font-family:monospace;font-size:13px;word-break:break-all;margin-bottom:8px} .token-box{background:#0f0f10;border:1px solid #2a2a2b;border-radius:8px;padding:12px 16px;font-family:monospace;font-size:13px;word-break:break-all;margin-bottom:8px}
.warn{color:#888;font-size:12px;margin:4px 0 20px} .warn{color:#888;font-size:12px;margin:4px 0 20px}
.instruct{color:#a1a1aa;font-size:13px;line-height:1.6;margin:8px 0}
.instruct code{background:#0f0f10;border:1px solid #2a2a2b;border-radius:4px;padding:2px 5px;font-size:12px}
.instruct ol{margin:8px 0;padding-left:20px}
.instruct li{margin:6px 0}
.client-section{border-top:1px solid #2a2a2b;padding-top:18px;margin-top:18px}
.client-section:first-of-type{border-top:none;padding-top:0;margin-top:0}
</style> </style>
</head> </head>
<body> <body>
@@ -798,11 +822,28 @@ pre{background:#0f0f10;border:1px solid #2a2a2b;border-radius:8px;padding:16px;f
<div class="token-box">${esc(token!)}</div> <div class="token-box">${esc(token!)}</div>
<p class="warn">Store this securely — it won't be shown again.</p> <p class="warn">Store this securely — it won't be shown again.</p>
<h2>Claude Desktop <code>claude_desktop_config.json</code></h2> <div class="client-section">
<pre id="claude-cfg">${esc(claudeConfig)}<button class="copy-btn" onclick="copy('claude-cfg')">Copy</button></pre> <h2>Claude.ai (browser)</h2>
<p class="instruct">In <a href="https://claude.ai" target="_blank" rel="noopener" style="color:#10a37f">claude.ai</a> go to <strong>Settings → Integrations → Add MCP server</strong> and paste:</p>
<pre id="claude-web-cfg">${esc(mcpUrl)}<button class="copy-btn" onclick="copy('claude-web-cfg')">Copy</button></pre>
<p class="instruct">When prompted, use the access token above.</p>
</div>
<h2>Codex CLI / opencode config</h2> <div class="client-section">
<pre id="codex-cfg">${esc(codexConfig)}<button class="copy-btn" onclick="copy('codex-cfg')">Copy</button></pre> <h2>Claude Desktop</h2>
<p class="instruct">Paste this into <code>claude_desktop_config.json</code>:</p>
<pre id="claude-cfg">${esc(claudeConfig)}<button class="copy-btn" onclick="copy('claude-cfg')">Copy</button></pre>
</div>
<div class="client-section">
<h2>ChatGPT / GPT Actions</h2>
<p class="instruct">For ChatGPT, use the OpenAPI spec at <code>${esc(serverUrl!)}/openapi.json</code> and add a Bearer token header with the token above. Native MCP support in chatgpt.com is not yet available.</p>
</div>
<div class="client-section">
<h2>Codex CLI / OpenCode</h2>
<pre id="codex-cfg">${esc(codexConfig)}<button class="copy-btn" onclick="copy('codex-cfg')">Copy</button></pre>
</div>
</div> </div>
<script> <script>
function copy(id) { function copy(id) {