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>
    </>
  );
}
