Building a Real MCP Server: A QR Code Generator Example
From zero to a working MCP tool, with a QR code generator as the example
Why an MCP server, and why QR codes
MCP (Model Context Protocol) is the thing that lets an AI agent — Claude Desktop, GitHub Copilot, your own agent code — call a tool over a standard protocol instead of you hand-wiring a one-off integration for every model. Point an MCP-aware client at a URL, and it can ask "what can you do?" and get back a machine-readable answer, with no docs, no SDK-specific glue code, no prior knowledge of what's on the other end.
We wanted a from-scratch example for this post, and we already had a good building block sitting in our own QR code generator page: dot styles, corner styles, gradients, an embeddable logo — a real option set, not a toy hello world. So instead of inventing a new example, we turned that existing generator into an MCP tool. Same underlying library (qr-code-styling), just called by an agent instead of a browser click.
The whole thing lives in toyapi, our backend API repo, as one new file: devtoolsdaily/qrCodeMcpApi.js.
Step 1: Describe the tool's input with a schema
MCP tools describe their inputs as JSON Schema, so a client can build a form (or fill in arguments) without you writing any documentation by hand. The official SDK lets you define that schema with Zod and it derives the JSON Schema for you:
import * as z from 'zod/v4';
const inputSchema = z.object({
data: z.string().min(1).max(4000).describe('The text or URL to encode'),
size: z.number().int().min(64).max(2048).default(512).describe('Output width and height in pixels'),
format: z.enum(['png', 'svg']).default('png').describe('Image format to return'),
dotsType: z.enum(['square', 'rounded', 'dots', 'classy', 'classy-rounded', 'extra-rounded'])
.default('square')
.describe("Style of the QR code's data dots"),
dotsColor: z.string().regex(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/).default('#000000')
.describe('Color of the data dots (and corners, unless overridden below)'),
// ...corner styles, gradient, background color, optional logo, error-correction level
});
Every .describe() call ends up as a description field in the JSON Schema a client discovers. That's the whole point — the field descriptions are the documentation an agent reads.
Step 2: Register the tool
import { McpServer } from '@modelcontextprotocol/server';
const server = new McpServer({ name: 'devtoolsdaily-qrcode', version: '1.0.0' });
server.registerTool(
'generate_qr_code',
{
description: 'Generate a customizable QR code (dot/corner styles, colors, gradient, optional embedded logo) and return it as an image.',
inputSchema,
},
async (input) => {
const { buffer, mimeType } = await generateQrCode(input);
return { content: [{ type: 'image', data: buffer.toString('base64'), mimeType }] };
}
);
The QR rendering itself is just qr-code-styling, the same library the web page uses — run server-side with canvas for PNG output and jsdom for SVG, instead of in a browser DOM.
Step 3: Mount it as an HTTP route
This is the part that surprised us with how little code it took. The SDK's Node adapter turns the whole MCP handshake — initialize, tools/list, tools/call, session management — into a single request handler you drop into an existing Express app:
import { createMcpHandler } from '@modelcontextprotocol/server';
import { toNodeHandler } from '@modelcontextprotocol/node';
export function addQrCodeMcpApi(app) {
const handler = createMcpHandler(createServer);
const node = toNodeHandler(handler);
app.all('/mcp/qrcode', cors(), json({ limit: '5mb' }), (req, res) => {
void node(req, res, req.body);
});
}
No custom routing for each MCP method, no manual JSON-RPC parsing — the SDK does the protocol; our code is just "here's a tool, here's its schema, here's what running it does."
That's it. That's the entire server. It's live at https://api.devtoolsdaily.com/mcp/qrcode.
Testing it without writing a client
We didn't want to just trust that this worked — an MCP server that only looks right in the code isn't proof it speaks the protocol correctly. So we tested it two ways.
First, with our own MCP Inspector — a browser-based MCP client we built for exactly this (point it at any remote MCP server and poke around, no install). We added the QR server as a one-click example:

Clicking it, connecting, and selecting the tool shows the full input form generated from the Zod schema above — every field, type, default, and description, with zero hand-written UI code on our end:

Filling in a URL and a custom color and calling the tool returns the real response — a base64-encoded PNG with the correct MIME type:

Decoding that data field is a real, correctly-styled QR code — this is the actual output of that actual call, not a mockup:

Second — and this is the test that actually matters — we connected the real @modelcontextprotocol/client SDK (the same library a production MCP-aware agent uses) from a throwaway script with zero prior knowledge of our schema, and had it discover and call the tool on its own. Full handshake, schema discovery, tool call, correct image back. No custom code path, no special-casing — just what any agent pointed at the URL would do.
Try it yourself
Point any MCP client at:
https://api.devtoolsdaily.com/mcp/qrcode
using the Streamable HTTP transport. If your client config looks like this (Claude Desktop / Copilot style):
{
"mcpServers": {
"devtoolsdaily-qrcode": {
"url": "https://api.devtoolsdaily.com/mcp/qrcode"
}
}
}
...it should discover one tool, generate_qr_code, with the full option set described above. No API key, no auth — it's a demo.
Or skip installing anything and just use our MCP Inspector — the QR server is now one of the built-in examples.
Source
The whole server is one file: devtoolsdaily/qrCodeMcpApi.js in toyapi. If you're building your first MCP server, that's a reasonable amount of code to read start to finish in a few minutes.