CONCEPT · 35 minutes
Install the TypeScript MCP SDK and scaffold the support-triage server skeleton — the foundation for modules 03 and 04
When I built the support-automation system, each new integration started the same way: write custom serialization, wire up auth, handle errors in a bespoke format — then repeat everything for the next tool. The support team's knowledge base, the ticket system, the routing engine each had its own shape. Giving an AI access to any one of them meant building a one-off adapter.
MCP standardizes those connections. But the TypeScript SDK goes further: it gives you compile-time
proof that your handlers match the protocol. The request schemas — ListResourcesRequestSchema,
ReadResourceRequestSchema, and the tool equivalents — are TypeScript objects. When you pass one
to setRequestHandler, TypeScript checks that your callback receives and returns the right shape.
Wrong type? Build fails. No runtime surprises.
That compile-time safety is the reason to use the typed SDK over ad-hoc HTTP routes or the string-based API surface the older examples show.
The one rule: Every handler in this path uses the typed schema form —
server.setRequestHandler(ListResourcesRequestSchema, async () => ...) — never the
string-based form that passes a plain string like 'resources/list' as the first argument.
You will never see the wrong API surface in this course.
In this module you will install the SDK, configure TypeScript, and scaffold the support-triage server skeleton. Resources come in module 03; tools come in module 04. This module's job is getting the frame right so the rest has somewhere to attach.
The support-triage server needs one runtime dependency: the MCP SDK. Development tooling — TypeScript, tsx, Node.js types — is separate so it never ships.
Add "type": "module" to package.json (the MCP SDK uses ES module syntax). Then create
tsconfig.json. Two settings are critical: "module": "Node16" and
"moduleResolution": "Node16". The SDK imports its own modules with .js extensions — Node16
resolution handles those correctly; CommonJS resolution rejects them.
CommonJS will not work here. If you set "module": "CommonJS" or omit "type": "module"
from package.json, imports from @modelcontextprotocol/sdk/server/index.js will fail with
"Cannot find module." Node16 is the correct target for this SDK.
Add a dev script to package.json so you can run src/index.ts directly without compiling:
Then create the source directory: mkdir src.
The Server class is the core of every MCP server. It holds the server's identity and the
handler registry you will fill in later. Create src/index.ts:
The name field is visible to end users in tools like Claude Desktop. support-triage-server
tells a developer at a glance what this server does. my-server or mcp-server-1 tells them
nothing useful.
The Server instance now exists in memory and holds your handler registry. But it cannot receive
any messages yet. Before reading on, predict: what is still missing, and what would happen if you
tried to call a handler directly without it?
The server has no input/output channel — it is a registry with no connection to the outside world. Calling a handler directly would bypass the protocol entirely, skipping request parsing, error wrapping, and the response envelope that MCP clients expect. The missing piece is the transport.
Think of the transport as the power cord for a lamp. The lamp knows exactly what to do — emit
light on demand — but it does nothing until it is plugged in. StdioServerTransport is the
standard cord for local development: it connects your server's input and output to the process's
stdin and stdout, which is how Claude Desktop and other MCP clients communicate.
Never use console.log() in an MCP server. It writes to stdout, which the MCP client
reads as protocol data. Use process.stderr.write() for all status and debug output.
The complete src/index.ts at this stage is a 17-line file — intentionally minimal:
This is not a stub. It is a valid, running MCP server. It just has no resources or tools yet. When an AI client connects and asks "what resources do you have?", it will receive an empty list — which is the correct protocol response for a server with no handlers registered.
Run the server with tsx to confirm it starts without errors:
What you have: A valid MCP server that starts, connects its transport, and waits for AI clients. In module 03 you add KB article and ticket history resources. In module 04 you add classify, route, and escalate tools. Each module adds one layer to this skeleton.
If you were adding this MCP server to an existing TypeScript monorepo — one that already has
its own tsconfig.json and build pipeline — which parts of this setup would you reuse from
the monorepo, and which are specific to the MCP server?
The tsconfig.json Node16 settings and "type": "module" in package.json are requirements
of the MCP SDK. If the monorepo already uses Node16 ESM, you can add the server as a workspace
package and reuse the monorepo's build pipeline. The only truly MCP-specific code is what lives
in src/index.ts. Keeping the server code separate from the scaffolding is what makes it
portable across projects.
Application-level questions to confirm you understand the setup choices, not just the steps.
1. Which import path correctly brings in the MCP `Server` class?
Correct Answer: B
The SDK uses sub-path exports. The Server class lives at `server/index.js`. Using the package root (`@modelcontextprotocol/sdk`) fails to resolve the named export; the other two package names do not exist.
2. Why must status messages go to `process.stderr` instead of `console.log` in an MCP server?
Correct Answer: B
MCP clients receive messages over stdout. A stray `console.log` line is not valid JSON-RPC and the client will fail to parse it, breaking the connection. stderr is a separate channel that clients ignore.
3. What does `await server.connect(transport)` do at runtime?
Correct Answer: C
`connect()` wires the transport's read/write streams to the server's message dispatcher. After this call the server listens for incoming JSON-RPC messages on stdin and writes responses to stdout.
4. A Server instance with no handlers registered is...
Correct Answer: B
A handler-less server starts and runs correctly. It returns `{ resources: [] }` and `{ tools: [] }` — valid MCP protocol responses, just not useful yet. This is why building the skeleton first and adding handlers incrementally works.
5. Which TypeScript setting is required for the MCP SDK to resolve its `.js` module extensions?
Correct Answer: C
The SDK imports its own sub-modules with explicit `.js` extensions (ESM style). Node16 resolution allows this. CommonJS resolution rejects `.js` extensions on TypeScript imports; Bundler works for bundled output but not for direct Node.js execution.
6. In the power-cord analogy, what does `StdioServerTransport` connect?
Correct Answer: C
StdioServerTransport reads JSON-RPC messages from stdin and writes responses to stdout. This is the channel Claude Desktop and other MCP clients use. Without it, the Server instance is a handler registry with no way to receive requests.
7. The `name` field in `new Server({ name, version })` is...
Correct Answer: B
Claude Desktop and other MCP clients show the server name when listing available servers. A descriptive name like `support-triage-server` tells a developer what the server does at a glance. `my-server` or `server-1` conveys nothing.
8. After calling `await server.connect(transport)` with no handlers registered, what does the server return when an AI client asks for the list of resources?
Correct Answer: C
The MCP SDK has a built-in default handler for `resources/list` that returns an empty array. An empty list is a perfectly valid response that tells the client 'this server exists but has no resources yet.' This is why the incremental build approach works.
Build a minimal MCP server that exposes a single hardcoded resource — a welcome message at info://welcome — using the typed schema-based handlers.
Steps:
Server, StdioServerTransport, ListResourcesRequestSchema, and ReadResourceRequestSchemaServer instance named 'welcome-server' version '1.0.0'ListResourcesRequestSchema handler returning one resource (uri 'info://welcome', name 'Welcome Message', mimeType 'text/plain')ReadResourceRequestSchema handler returning 'Welcome to MCP!' for the welcome URI and throwing for any other URIStdioServerTransport, connect the server, and write a status line to process.stderrHint: ListResourcesRequestSchema returns {resources: [...]}. ReadResourceRequestSchema returns {contents: [{uri, mimeType, text}]}. If the URI does not match, throw new Error("Resource not found: " + uri).
ListResourcesRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
const server = new Server({ name: "welcome-server", version: "1.0.0" });
// TODO: add ListResourcesRequestSchema handler // TODO: add ReadResourceRequestSchema handler
const transport = new StdioServerTransport(); await server.connect(transport); process.stderr.write("welcome-server running\n"); </CodeExample>
Worked Solution
<CodeExample title="Solution" language="typescript"> server.setRequestHandler(ListResourcesRequestSchema, async () => ({ resources: [ { uri: "info://welcome", name: "Welcome Message", mimeType: "text/plain" }, ], }));server.setRequestHandler(ReadResourceRequestSchema, async (req) => { const { uri } = req.params; if (uri !== "info://welcome") { throw new Error("Resource not found: " + uri); } return { contents: [{ uri, mimeType: "text/plain", text: "Welcome to MCP!" }], }; }); </CodeExample>
With the server skeleton in place, module 03 fills in the resources. You will use
ListResourcesRequestSchema and ReadResourceRequestSchema — the typed schema handlers that give
the support-triage server its first real capability: answering AI queries about KB articles and
recent ticket history.
The pattern you will use in module 03 is the same pattern this module establishes: import the
schema, pass it to setRequestHandler, implement the typed callback. The typed API surface you
learned here is the foundation the rest of the path builds on.