Large Language ModelsGenerate imagesGenerate videos
Create an MCP Server for Claude Code and GitHub Copilot
Build a single TypeScript MCP server and register it in both Claude Code and GitHub Copilot. You get working tool code, the exact config for each client, a debugging routine with the MCP Inspector, and an image tool that calls the PicassoIA API.
You wrote a script that saves you ten minutes a day, and now you want your AI assistant to run it without a copy and paste detour. Build it once as a Model Context Protocol server and both Claude Code and GitHub Copilot can call the same tools, because MCP is the shared language they use to talk to anything outside the editor. This walkthrough builds a small TypeScript server, registers it in Claude Code, registers it in Copilot inside VS Code, and finishes with a real image tool that calls the PicassoIA API. Plan on about 40 minutes and roughly 100 lines of code.
Why One Server Beats Two
Before MCP, every assistant demanded its own plugin format, its own manifest and its own packaging rules. An MCP server replaces that pile with a single process that advertises what it can do. The client starts the process, asks for its list of capabilities, and hands that list to the model. The model then decides, mid-conversation, when a call is worth making.
Same Protocol, Two Clients
A server can expose three kinds of capability:
Tools: functions the model can call, such as add_note or generate_image.
Resources: read-only data the client can attach to a conversation, such as a log file or a schema.
Prompts: reusable templates the user triggers on purpose.
Tools are where almost all the value sits today, so this article stays focused on them. Both Claude Code and Copilot speak the same JSON-RPC messages over the same transports, which means a server that passes one client will pass the other with almost no changes.
Where the Setups Differ
The server is identical. The registration is not. Here is the whole difference in one table:
Setting
Claude Code
GitHub Copilot in VS Code
Config file
.mcp.json in the project, or ~/.claude.json
.vscode/mcp.json, or your user profile
Root property
mcpServers
servers
Add from the terminal
claude mcp add
Command Palette: MCP: Add Server
Transport field
type (stdio, http, sse)
type is required (stdio or http)
Secrets
--env flag or ${VAR} expansion
inputs block with ${input:id}
Where tools run
Any session
Agent mode only
💡 Tip: The root property is the classic trap. Paste a Claude Code config into VS Code unchanged and nothing loads, because Copilot looks for servers, not mcpServers.
Set Up the Project
Pick a folder outside your main repository so the server can serve several projects later. You need Node.js 20 or newer and a terminal.
The examples use the @modelcontextprotocol/sdk 1.x API with McpServer and registerTool. If a newer major version changes an import path, the concepts below stay the same.
Start With stdio
MCP defines two main transports. stdio means the client launches your server as a child process and exchanges messages through standard input and output. Streamable HTTP means the server runs on its own and clients connect by URL. Start with stdio. It needs no port, no authentication layer and no hosting, and both clients support it out of the box. Move to HTTP only when several people must share one running instance.
Write the Server
Our example is a tiny team notes server with two tools: one saves a note, one searches them. It is small enough to read in a minute and real enough to be useful.
Register a Tool
Create src/index.ts:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { promises as fs } from "node:fs";
import path from "node:path";
const NOTES_FILE = path.join(process.env.NOTES_DIR ?? process.cwd(), "notes.json");
type Note = { id: number; text: string; tags: string[]; createdAt: string };
async function readNotes(): Promise<Note[]> {
try {
return JSON.parse(await fs.readFile(NOTES_FILE, "utf8"));
} catch {
return [];
}
}
const server = new McpServer({ name: "team-notes", version: "1.0.0" });
server.registerTool(
"add_note",
{
title: "Add note",
description:
"Save a short engineering note with optional tags. Use it when the user asks to remember a decision, a command or a bug.",
inputSchema: {
text: z.string().min(3).max(2000),
tags: z.array(z.string()).default([]),
},
},
async ({ text, tags }) => {
const notes = await readNotes();
const note: Note = {
id: notes.length + 1,
text,
tags,
createdAt: new Date().toISOString(),
};
await fs.writeFile(NOTES_FILE, JSON.stringify([...notes, note], null, 2));
return { content: [{ type: "text", text: `Saved note #${note.id}` }] };
}
);
server.registerTool(
"search_notes",
{
title: "Search notes",
description: "Find saved notes whose text or tags contain the query.",
inputSchema: { query: z.string().min(1) },
},
async ({ query }) => {
const q = query.toLowerCase();
const hits = (await readNotes()).filter(
(n) =>
n.text.toLowerCase().includes(q) ||
n.tags.some((t) => t.toLowerCase().includes(q))
);
const text = hits.length
? hits.map((n) => `#${n.id} [${n.tags.join(", ")}] ${n.text}`).join("\n")
: "No notes matched.";
return { content: [{ type: "text", text }] };
}
);
await server.connect(new StdioServerTransport());
console.error("team-notes MCP server running on stdio");
Run npm run build. You now have dist/index.js, and that file is the only thing the two clients need to know about.
Return Clean Results
The model reads whatever you return, so treat the return value as an interface. Keep results short, structured and honest. When something fails, do not throw an exception that dies in the transport layer. Return an error the model can read and react to:
return {
isError: true,
content: [{ type: "text", text: "notes.json is not valid JSON. Fix or delete it." }],
};
An isError result lets the assistant explain the problem to you or retry with different input. A crash just shows a vague "server disconnected" banner.
Keep stdout Silent
This is the single most common reason a first server fails. With stdio, standard output belongs to the protocol. One stray console.log injects plain text into the JSON-RPC stream and the client drops the connection. Log to console.error, which writes to stderr, and both clients will capture it as diagnostic output instead.
💡 Tip: Write tool descriptions as instructions to the model, not as documentation for humans. "Use it when the user asks to remember a decision" gets the tool picked at the right moment. "Notes utility" does not.
Connect Claude Code
Add It With the CLI
One command registers the server. Options go before the name, and a double dash separates the name from the command Claude Code will launch:
claude mcp add --transport stdio --scope user \
--env NOTES_DIR=/home/dev/notes \
team-notes -- node /absolute/path/to/team-notes-mcp/dist/index.js
Use an absolute path. Claude Code starts the process from whatever directory your session uses, so relative paths break the moment you open a different project. Then confirm it:
claude mcp list
claude mcp get team-notes
Inside a session, type /mcp to see connection status and the list of tools. Ask something natural, like "Remember that we deploy on Thursdays, tag it release", and watch Claude Code request permission to call add_note.
Share It Through .mcp.json
The scope flag decides who gets the server. local keeps it private to you in one project, user makes it available in every project, and project writes a .mcp.json file you can commit so the whole team gets it. Here is a shared config that avoids hardcoded paths:
Each teammate sets TEAM_NOTES_PATH once in their shell. The ${NOTES_DIR:-.notes} form supplies a default when the variable is missing. Claude Code asks for approval the first time it sees a project-scoped server, which is a sensible safeguard for anything pulled from a repository.
Connect GitHub Copilot
Write .vscode/mcp.json
Create .vscode/mcp.json in your workspace. Remember the different root property and the required type:
${workspaceFolder} makes the file portable, so you can commit it. For secrets, add an inputs array. VS Code prompts once, stores the value securely and injects it:
Copilot Chat opens in Ask mode by default, and MCP tools only fire in Agent mode. Switch the mode in the chat panel, open the tools picker, and confirm that team-notes shows up with both tools ticked. If it does not appear, run MCP: List Servers from the Command Palette, pick the server, and read its output. Restart it from the same menu after every rebuild.
Copilot also lets you choose among the models your plan offers, so the same server gets exercised by different models. That is a cheap way to check whether your tool descriptions are clear enough for every one of them.
Test and Debug Before Shipping
Run the MCP Inspector
Before blaming either client, test the server alone. The official Inspector opens a local web page where you can list tools, fill in arguments and see raw responses:
Call add_note with an empty text. Your Zod schema should reject it with a readable validation message. Then call search_notes with a tag you just saved. If both behave here, any remaining problem lives in client configuration, not in your code.
Fix the Usual Failures
Symptom
Likely cause
Fix
Server never connects
A console.log wrote to stdout
Switch to console.error
"Command not found"
Relative path or missing build
Use an absolute path and run npm run build
Tools missing in Copilot
Chat is in Ask mode
Switch to Agent mode
Tool exists but is never chosen
Vague description
Rewrite it with trigger phrases
Empty environment variable
Not declared in the config
Add it to the env block
Image calls fail under load
More than 5 jobs at once
Queue calls inside the tool
Give Your Server an Image Tool
Notes are fine, but the best demonstration of MCP is a tool that does something the assistant cannot do alone. Image generation is a good fit: the model writes a precise prompt, your server turns it into a file, and the URL lands straight back in the conversation.
Call the PicassoIA API
The PicassoIA developer API lives at https://api.picassoia.com/v1 and uses a Bearer token that starts with pia_sk_. Predictions are asynchronous and Replicate-style: you create one, then poll it until the status reads succeeded. The PicassoIA Image model accepts a prompt of up to 4,000 characters, an aspect_ratio, and returns a list of image URLs. Add this tool before the server.connect line:
An account allows 5 concurrent predictions, shared across every token and connection, so a loop that fires ten images at once will hit that ceiling. Generate them one after another inside the tool, or keep a small queue.
💡 Tip: Check the current pricing and plan requirements on the PicassoIA API page before you publish a server for others. The docs and the pricing page describe access differently, so confirm what your own plan includes.
How to Use Sonnet 5 on PicassoIA
Your tool descriptions are prompts, and a language model is the best editor for them. Claude Sonnet 5 is a strong pick for this job, and you can run it on PicassoIA without leaving the browser.
Open the Claude Sonnet 5 page in the large language models collection.
Paste your tool definitions, including names, descriptions and schemas, into the prompt.
Ask it to rewrite each description as a short instruction that states when to call the tool and what it returns.
Ask for ten edge case inputs per tool, such as empty strings, very long text and unusual tags.
Run those inputs through the MCP Inspector, fix every failure, and paste the improved descriptions back into your code.
For a second opinion on tricky logic, Claude Fable 5 and GPT 5.6 Sol are both listed for coding tasks. Comparing their rewrites of one description often reveals which phrasing is ambiguous.
Try It Yourself on PicassoIA
You now have one server running in two assistants: notes for memory, an image tool for output, and a testing routine that keeps both honest. The same pattern scales to anything you can wrap in a function, from deploy scripts to database lookups.
Start with the image tool, because it gives instant feedback. Write a prompt, call generate_image from Claude Code or Copilot, and see what comes back in seconds. Then open the PicassoIA Image page and experiment with aspect ratios and prompt styles directly, or browse every available model at picassoia.com/en/all-models. Your first image is a single prompt away.