Course · Claude Code · Extending Claude Code
Connect an external system with an MCP server
You are debugging a failed charge in a payments service, and the two systems that hold the answers - the team’s staging Postgres and the ticket board - sit outside the agent’s reach. The conversation settles into a relay. You paste the ticket text, the agent asks what the charges table looks like, you run a psql query and paste the schema back, it guesses a column name and guesses wrong. Every fact from those systems passes through you, by hand.
One thing to be straight about: the small TypeScript payments service this course uses as its example is self-contained. No file in it opens a database connection, and there is no ticket board. The two systems in this lesson are a constructed scenario, the surroundings a real payments team would have. The commands below are real; the systems they point at are the imagined team’s, and you substitute your own.
The relay is the signal. The Model Context Protocol (MCP) is an open standard for connecting an agent to an external tool or data source; an MCP server is one such connection. Add the database as a server and the agent queries the schema itself. Add the board and it reads the ticket without you relaying it.
Register the two servers
Section titled “Register the two servers”You register a server with claude mcp add. Two transport shapes meet you immediately, and our two systems land one on each.
A local stdio server runs as a process on your machine - the right shape for a tool that needs direct system access, like a database client. Postgres through the DBHub server is one of these:
claude mcp add --transport stdio db -- npx -y @bytebase/dbhub \ --dsn "postgresql://readonly:pass@localhost:5432/payments"Read the command’s shape carefully, because the ordering bites people. Options like --transport, --scope, and --env come before the server name; everything after the bare -- is the command Claude Code spawns to run the server, passed through untouched. The sharpest documented trap is --env: if the server name directly follows an --env pair, the CLI can read the name as another pair, so keep another option between them. Full syntax is on the MCP reference. Note the readonly user in that connection string - the next lesson explains why.
A remote HTTP server is the recommended shape for a cloud service, and most hosted tools expose one. You point it at the URL the vendor publishes for their MCP endpoint:
claude mcp add --transport http jira "https://<your-board-mcp-endpoint>"The quotes are part of the command, not decoration: the angle-bracket placeholder marks a URL you substitute, and an unquoted < is an input redirection to the shell - the line is a syntax error before claude even runs.
Check the vendor’s own MCP docs for the exact endpoint; they publish it, so do not guess. Cloud servers usually need authentication. HTTP MCP supports OAuth through the /mcp panel or claude mcp login <name>, static headers with --header, and server-specific helpers for short-lived tokens. Follow the server’s documentation, and keep secrets out of checked-in config files.
The SSE and WebSocket transports, and claude mcp add-json for pasting a complete config, are all on the MCP docs page. The commands you will use daily are claude mcp add, claude mcp list, and claude mcp get <name>; inside a session, /mcp shows each registered server’s status and authentication.
Ask one question across both systems
Section titled “Ask one question across both systems”Once a server connects, its tools appear alongside the built-ins, and the agent reaches for them the way it reaches for Read or Bash. Here is an illustrative exchange showing the shape of the collapse - invented particulars, real mechanism:
> charge ch_8812 failed for this customer - look at the ticket and the db, what's going on?
● jira - get_issue (PAY-441) "Charge rejected: customer reports card was valid"
● db - execute_sql SELECT status, failure_code FROM charges WHERE id = 'ch_8812' → status=failed, failure_code=expired_idempotency_key
The ticket says the card was valid, and the database agrees - the decline isn't a card problem. failure_code is expired_idempotency_key, so the retry arrived after the key's TTL. That's our bug, not the bank's.One question in plain language, and the agent pulled the issue and the row on its own, then reasoned across both. Nothing was pasted. Two systems that lived entirely in your head a minute ago are now part of the agent’s working context: the information it can draw on when it decides. The example repo does carry a real idempotency defect - checkIdempotency in src/utils/idempotency.ts records a key as used at first check, before the payment completes - but this particular failure_code and row belong to the constructed scene, not a captured run.
What a mounted server costs
Section titled “What a mounted server costs”Reach is not free. Each mounted server contributes tool definitions to the agent’s context, so a connection you never use still costs window space - the finite capacity of that working context - on every session, although tool search defers loading full tool definitions until they are needed. Providers and models differ, so check the real cost with /context (the costs guide covers reading it) rather than assuming every server has the same startup footprint. The meter below uses order-of-magnitude schema sizes, not measured ones. Mount and unmount servers and watch what each one costs:
You have handed the agent reach into two systems it could not see. That should feel slightly uncomfortable: a thing that acts on its own now holds a live connection to a production-adjacent database and your ticket tracker. A connection is not the same as unrestricted trust, and drawing that line deliberately is the next lesson.