190 lines
8.2 KiB
TypeScript
190 lines
8.2 KiB
TypeScript
|
|
import { JsonLd } from '@/components/json-ld';
|
||
|
|
import { StaticCodeBlock } from '@/components/static-code-block';
|
||
|
|
import { articleJsonLd, breadcrumbJsonLd, pageMetadata } from '@/lib/seo';
|
||
|
|
import Link from 'next/link';
|
||
|
|
import { ArticleShell, H2, Note, OL, P, Strong, Table, UL } from '../article-shell';
|
||
|
|
|
||
|
|
const PATH = '/guides/claude-desktop-mcp-setup';
|
||
|
|
const TITLE = 'Connect a custom MCP server to Claude Desktop (step by step)';
|
||
|
|
const DESCRIPTION =
|
||
|
|
'How to add a remote MCP server to Claude Desktop: the config snippet, the OAuth flow on first use, and a troubleshooting table for 401s, missing servers and invisible tools.';
|
||
|
|
|
||
|
|
export const metadata = pageMetadata({ title: TITLE, description: DESCRIPTION, path: PATH });
|
||
|
|
|
||
|
|
const CONFIG_SNIPPET = `{
|
||
|
|
"mcpServers": {
|
||
|
|
"my-tools": {
|
||
|
|
"url": "https://my-tools-a1.mcp.buildmymcpserver.com/mcp",
|
||
|
|
"auth": "oauth2"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}`;
|
||
|
|
|
||
|
|
const LOCAL_VS_REMOTE = `# Local (STDIO) — runs on your machine, per-machine setup
|
||
|
|
"command": "npx", "args": ["-y", "@your/mcp-server"]
|
||
|
|
|
||
|
|
# Remote (Streamable HTTP) — hosted, one URL for every machine
|
||
|
|
"url": "https://my-tools-a1.mcp.buildmymcpserver.com/mcp"`;
|
||
|
|
|
||
|
|
export default function Page() {
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
<JsonLd
|
||
|
|
data={articleJsonLd({
|
||
|
|
title: TITLE,
|
||
|
|
description: DESCRIPTION,
|
||
|
|
path: PATH,
|
||
|
|
datePublished: '2026-07-08',
|
||
|
|
authorName: 'Marco Sadjadi',
|
||
|
|
wordCount: 1400,
|
||
|
|
})}
|
||
|
|
/>
|
||
|
|
<JsonLd
|
||
|
|
data={breadcrumbJsonLd([
|
||
|
|
{ name: 'Home', path: '/' },
|
||
|
|
{ name: 'Guides', path: '/guides' },
|
||
|
|
{ name: TITLE, path: PATH },
|
||
|
|
])}
|
||
|
|
/>
|
||
|
|
<ArticleShell
|
||
|
|
title={TITLE}
|
||
|
|
subtitle="Claude Desktop supports both local STDIO servers and remote servers over Streamable HTTP. Remote is the version that survives a laptop change — here is the exact setup, including what the OAuth consent screen is doing."
|
||
|
|
updated="July 2026"
|
||
|
|
>
|
||
|
|
<H2>Local vs. remote — pick remote unless you have a reason</H2>
|
||
|
|
<P>
|
||
|
|
Most MCP tutorials wire up a <Strong>local STDIO server</Strong>: Claude Desktop spawns a
|
||
|
|
process on your machine and talks to it over stdin/stdout. That works, but it is
|
||
|
|
per-machine — every teammate repeats the setup, secrets live in local config files, and
|
||
|
|
nothing works from a second device. A <Strong>remote server over Streamable HTTP</Strong>{' '}
|
||
|
|
is one URL that every installation shares, with auth handled by OAuth instead of
|
||
|
|
plaintext keys in a JSON file.
|
||
|
|
</P>
|
||
|
|
<StaticCodeBlock code={LOCAL_VS_REMOTE} label="the difference in config terms" />
|
||
|
|
|
||
|
|
<H2>Step 1 — get a server URL</H2>
|
||
|
|
<P>
|
||
|
|
You need a live MCP endpoint. If you already run one, use its URL. If not, you can{' '}
|
||
|
|
<Link href="/guides/create-mcp-server-without-code" className="text-[--color-accent] hover:underline">
|
||
|
|
generate one from a prompt
|
||
|
|
</Link>{' '}
|
||
|
|
or fork a working one from{' '}
|
||
|
|
<Link href="/templates" className="text-[--color-accent] hover:underline">
|
||
|
|
the template gallery
|
||
|
|
</Link>{' '}
|
||
|
|
— either way you end up with an OAuth-protected URL like{' '}
|
||
|
|
<Strong>https://my-tools-a1.mcp.buildmymcpserver.com/mcp</Strong>.
|
||
|
|
</P>
|
||
|
|
|
||
|
|
<H2>Step 2 — add the server to Claude Desktop</H2>
|
||
|
|
<OL>
|
||
|
|
<li>
|
||
|
|
Open Claude Desktop settings and go to the connectors/MCP section (on recent versions:
|
||
|
|
Settings → Connectors → Add custom connector), or edit{' '}
|
||
|
|
<Strong>claude_desktop_config.json</Strong> directly.
|
||
|
|
</li>
|
||
|
|
<li>Add the server entry:</li>
|
||
|
|
</OL>
|
||
|
|
<StaticCodeBlock code={CONFIG_SNIPPET} label="claude_desktop_config.json" />
|
||
|
|
<OL>
|
||
|
|
<li value={3}>Restart Claude Desktop. Config is read at startup, not live.</li>
|
||
|
|
</OL>
|
||
|
|
<Note>
|
||
|
|
Claude Desktop's settings UI changes between releases; the JSON config path is the stable
|
||
|
|
fallback. On macOS it lives at{' '}
|
||
|
|
<Strong>~/Library/Application Support/Claude/claude_desktop_config.json</Strong>, on
|
||
|
|
Windows at <Strong>%APPDATA%\Claude\claude_desktop_config.json</Strong>.
|
||
|
|
</Note>
|
||
|
|
|
||
|
|
<H2>Step 3 — the OAuth flow on first use</H2>
|
||
|
|
<P>
|
||
|
|
The first time Claude touches the server it will receive a <Strong>401</Strong> — that is
|
||
|
|
correct behavior, not an error. The client then discovers the authorization server,
|
||
|
|
registers itself via Dynamic Client Registration, and opens a browser window for consent.
|
||
|
|
You approve once; the client stores the token and refreshes it silently afterwards.
|
||
|
|
</P>
|
||
|
|
<P>
|
||
|
|
Under the hood this is the OAuth 2.1 handshake the MCP spec requires for remote servers:
|
||
|
|
PKCE so the code exchange cannot be intercepted, and Resource Indicators (RFC 8707) so a
|
||
|
|
token issued for this server cannot be replayed against another. Details in{' '}
|
||
|
|
<Link href="/docs/oauth" className="text-[--color-accent] hover:underline">
|
||
|
|
the OAuth documentation
|
||
|
|
</Link>
|
||
|
|
.
|
||
|
|
</P>
|
||
|
|
|
||
|
|
<H2>Step 4 — verify the tools are there</H2>
|
||
|
|
<UL>
|
||
|
|
<li>Open a new conversation and check the tools/connectors icon — your server should be listed with its tools.</li>
|
||
|
|
<li>
|
||
|
|
Ask Claude directly: <em>“Use the search_pages tool to find X.”</em> Naming the tool
|
||
|
|
forces Claude to attempt the call instead of answering from memory.
|
||
|
|
</li>
|
||
|
|
<li>Watch your server's dashboard logs — you should see the tool call arrive with latency and status.</li>
|
||
|
|
</UL>
|
||
|
|
|
||
|
|
<H2>Troubleshooting</H2>
|
||
|
|
<Table>
|
||
|
|
<thead>
|
||
|
|
<tr>
|
||
|
|
<th>Symptom</th>
|
||
|
|
<th>Likely cause</th>
|
||
|
|
<th>Fix</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody>
|
||
|
|
<tr>
|
||
|
|
<td>Server never appears in the client</td>
|
||
|
|
<td>Config JSON is invalid, or Claude was not restarted</td>
|
||
|
|
<td>
|
||
|
|
Validate the JSON (trailing commas are the classic), restart Claude Desktop fully
|
||
|
|
(quit, not close window).
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Endless 401 loop, consent screen never opens</td>
|
||
|
|
<td>URL points at the server root instead of the /mcp endpoint</td>
|
||
|
|
<td>Use the full endpoint URL ending in /mcp, exactly as the install snippet shows.</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Consent worked, tools still missing</td>
|
||
|
|
<td>Server is deployed but tool listing failed on connect</td>
|
||
|
|
<td>
|
||
|
|
Check the server's live logs for an initialize/list_tools error; redeploy if the
|
||
|
|
container restarted with a bad secret.
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Tool listed, every call errors</td>
|
||
|
|
<td>Upstream credential (e.g. your API key) is wrong or expired</td>
|
||
|
|
<td>
|
||
|
|
Update the secret in the dashboard — secrets are injected at runtime, so a redeploy
|
||
|
|
picks up the new value. They are never shown back, so re-enter rather than inspect.
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
<tr>
|
||
|
|
<td>Worked yesterday, 401 today</td>
|
||
|
|
<td>Token expired and silent refresh failed</td>
|
||
|
|
<td>Remove and re-add the connector to force a fresh OAuth flow.</td>
|
||
|
|
</tr>
|
||
|
|
</tbody>
|
||
|
|
</Table>
|
||
|
|
|
||
|
|
<H2>One server, every client</H2>
|
||
|
|
<P>
|
||
|
|
The same URL works in Cursor, VS Code Copilot, Continue.dev and{' '}
|
||
|
|
<Link href="/guides/chatgpt-mcp-connector" className="text-[--color-accent] hover:underline">
|
||
|
|
ChatGPT custom connectors
|
||
|
|
</Link>{' '}
|
||
|
|
— the point of hosting a remote MCP server is that the client config is the only
|
||
|
|
per-client step left. If you hit a case this guide does not cover, the{' '}
|
||
|
|
<Link href="/docs/faq" className="text-[--color-accent] hover:underline">
|
||
|
|
docs FAQ
|
||
|
|
</Link>{' '}
|
||
|
|
collects the rarer ones.
|
||
|
|
</P>
|
||
|
|
</ArticleShell>
|
||
|
|
</>
|
||
|
|
);
|
||
|
|
}
|