Development Environment
By the Syncfusion DevTutorials team · Last updated
The tools for building MCP servers: the TypeScript and Python SDKs, the MCP Inspector, project structure, environment variables and secrets, pinned dependencies, and publishing to the MCP Registry.
Before a carpenter builds a table, they set up the workshop: a good saw, a measuring tape, a clean bench, and a place to keep finished pieces. Good tools make good work faster and help catch mistakes early.
The same is true for an MCP server. A good server setup includes an SDK that handles the protocol, a way to test the server, a simple project layout, safe configuration, and a way to share metadata when you want others to discover it.
The main idea: pick an official SDK, test with the MCP Inspector, store secrets in environment variables, pin versions intentionally, and publish metadata to the MCP Registry when your server is ready to be discovered.
A Real-World Example
Ravi is starting a new server for Brightgrid's returns team.
- He chooses the TypeScript SDK v2 and pins exact versions for the packages his server depends on.
- He creates a project layout with one module per area and a separate
testfolder. - He keeps the API key in an environment variable, not in source code.
- Before connecting the server to any AI app, he runs the MCP Inspector to list tools, call them, and inspect the raw protocol messages.
- When the server is ready for external customers, he publishes the server metadata to the MCP Registry.
In simple words: the SDK handles MCP details, the Inspector checks the behavior, and the Registry helps other hosts find the server.
What You Will Learn
In this lesson, you will learn:
- The official SDKs and the packages they provide.
- How to use the MCP Inspector to test a server.
- A clean project structure for a long-lived server.
- How environment variables and secrets should be managed.
- How dependency pinning helps keep builds stable.
- What the MCP Registry is and how a server is published there.
The MCP Lab at the bottom of the page includes a lightweight inspector with Explorer, Inspector, and Raw request views. The real MCP Inspector does the same job for servers running locally or on a network.
SDKs
A server moves from code to discovery: write with the SDK, test the handlers, inspect the request flow, try it in a host, and publish metadata to the Registry.
TypeScript
The official TypeScript SDK v2 is split into a few packages.
| Package | What it is for |
|---|---|
@modelcontextprotocol/server | Building servers, including McpServer, registerTool, createMcpHandler, and the stdio transport |
@modelcontextprotocol/client | Building clients and hosts |
@modelcontextprotocol/node | Serving HTTP from Node.js with helpers like toNodeHandler |
zod | Defining input and output schemas for tools |
This course's lab uses version 2.1.0 of the server and client packages.
Python
The official Python SDK is the mcp package, currently at version 2. You build a server with MCPServer and decorators such as @mcp.tool(). Type hints on the function become the input schema, which keeps the server definition close to the tool logic.
Other Languages
The MCP protocol is language-independent. As long as a server follows the specification, it can communicate with hosts written in other languages.
There are official SDKs for other languages too, including C#, Go, and Java. They follow the same protocol rules, so a server written in one language can still work with a client written in another.
Check which specification version each SDK release supports. A new protocol release, such as 2026-07-28, may take time to reach every SDK and every transport. In the TypeScript SDK 2.1.0, for example, stdio connections still begin with the older initialize step.
The MCP Inspector
What It Does
The MCP Inspector is the official testing tool for MCP servers. It connects to a server, lists tools, resources, and prompts, lets you call them, and shows the raw protocol messages. This is a great way to test the server without relying on a model to explain what happened.
The Inspector has three main interfaces:
- a web UI:
npx @modelcontextprotocol/inspector - a CLI for scripts and CI:
npx @modelcontextprotocol/inspector --cli ... - a terminal UI:
--tui
It needs a recent version of Node.js. For Inspector v2, that means Node.js 22.19 or newer.
Quick Checks with the CLI
The CLI takes the server command or URL and then a method name.
Inspector CLI commands
function inspectorCmd(target, method, extra = {}) {
const parts = ["npx @modelcontextprotocol/inspector --cli", target, "--method", method];
if (extra.tool) {
parts.push("--tool-name", extra.tool);
}
for (const [k, v] of Object.entries(extra.args ?? {})) {
parts.push("--tool-arg", `${k}=${v}`);
}
if (extra.uri) {
parts.push("--uri", extra.uri);
}
return parts.join(" ");
}
console.log(inspectorCmd("node build/index.js", "tools/list"));
console.log(
inspectorCmd("node build/index.js", "tools/call", {
tool: "lookup_order",
args: { order_id: "A-1001" },
}),
);
console.log(
inspectorCmd("http://127.0.0.1:3001/mcp", "resources/read", { uri: "kb://shipping" }),
);
Expected output:
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/list
npx @modelcontextprotocol/inspector --cli node build/index.js --method tools/call --tool-name lookup_order --tool-arg order_id=A-1001
npx @modelcontextprotocol/inspector --cli http://127.0.0.1:3001/mcp --method resources/read --uri kb://shipping
This is useful because the CLI prints the JSON response, which you can check in a CI script or a shell pipeline. That makes it easier to verify a server before a model or a host depends on it.
Project Structure
A Clean Layout
A small server can start as a single file, but a real project benefits from a clear layout. A simple, shared structure helps you find tools, resources, and prompts quickly.
A project layout
const tree = {
"brightgrid-mcp/": {
"package.json": "name, version, mcpName, dependencies, scripts",
"tsconfig.json": "TypeScript settings",
".env.example": "the settings it needs, with no real secrets",
"server.json": "metadata for the MCP Registry",
"src/": {
"index.ts": "creates the server and picks the transport",
"tools/orders.ts": "the order tools",
"resources/kb.ts": "the knowledge-base resources",
"prompts/replies.ts": "the prompts",
},
"test/": { "orders.test.ts": "unit tests for the handlers" },
},
};
function print(node, indent = "") {
for (const [name, value] of Object.entries(node)) {
if (typeof value === "string") {
console.log(`${indent}${name.padEnd(22 - indent.length)} ${value}`);
} else {
console.log(indent + name);
print(value, indent + " ");
}
}
}
print(tree);
Expected output:
brightgrid-mcp/
package.json name, version, mcpName, dependencies, scripts
tsconfig.json TypeScript settings
.env.example the settings it needs, with no real secrets
server.json metadata for the MCP Registry
src/
index.ts creates the server and picks the transport
tools/orders.ts the order tools
resources/kb.ts the knowledge-base resources
prompts/replies.ts the prompts
test/
orders.test.ts unit tests for the handlers
A useful structure usually includes:
- one module per area: tools, resources, prompts
- a
testfolder with direct tests for handlers and utilities .env.examplewith placeholders and no secretsserver.jsonwith metadata for the MCP Registry
Settings and Secrets
Environment Variables
Settings such as API URLs, keys, and ports should come from environment variables. The host passes them when it starts a stdio server, and a platform passes them when it runs an HTTP server. The server should validate the required values at startup and stop with a clear error if something is missing.
Load settings safely
function loadConfig(env) {
const problems = [];
function required(name) {
if (!(name in env)) {
problems.push(`${name} is missing`);
}
return env[name];
}
const config = {
apiUrl: required("BRIGHTGRID_API_URL"),
apiKey: required("BRIGHTGRID_API_KEY"),
port: Number(env.PORT ?? 3001),
logLevel: env.LOG_LEVEL ?? "info",
};
return problems.length ? { error: problems.join("; ") } : config;
}
const fromHost = {
BRIGHTGRID_API_URL: "https://api.brightgrid.example",
BRIGHTGRID_API_KEY: "sk-test-123",
PORT: "4000",
};
const shown = loadConfig(fromHost);
console.log(JSON.stringify({ ...shown, apiKey: shown.apiKey ? "[set]" : undefined }));
console.log(JSON.stringify(loadConfig({ PORT: "4000" })));
Expected output:
{"apiUrl":"https://api.brightgrid.example","apiKey":"[set]","port":4000,"logLevel":"info"}
{"error":"BRIGHTGRID_API_URL is missing; BRIGHTGRID_API_KEY is missing"}
The important lesson is simple: do not hard-code secrets into the application. If a key is in source code, it is not really a secret.
Never put secrets in your code, in server.json, in a tool description, or in a committed config file. Never print them in logs. If a secret leaks, revoke it immediately.
Dependencies
Pin What Matters
Use exact versions for the packages that are part of your server's contract, especially the MCP SDK itself. That makes each build repeatable. You can still use version ranges for local dev tools, but for the server package you should be deliberate about upgrades.
Exact versions and ranges
const dependencies = {
"@modelcontextprotocol/server": "2.1.0",
"@modelcontextprotocol/node": "2.1.0",
zod: "4.2.1",
};
const devDependencies = {
"@modelcontextprotocol/inspector": "^2.10.1",
typescript: "^5",
};
for (const [name, version] of Object.entries({ ...dependencies, ...devDependencies })) {
const kind = /^[\^~]/.test(version) ? "range " : "exact ";
console.log(kind, name.padEnd(32), version);
}
Expected output:
exact @modelcontextprotocol/server 2.1.0
exact @modelcontextprotocol/node 2.1.0
exact zod 4.2.1
range @modelcontextprotocol/inspector ^2.10.1
range typescript ^5
This practice matters because protocol changes and SDK behavior can move together. A pinned version helps you validate upgrades intentionally, instead of allowing the package manager to change your runtime unexpectedly.
The MCP Registry
What It Is
The MCP Registry is the official public catalog of MCP servers at registry.modelcontextprotocol.io. It is still in preview, so the exact behavior can evolve, but the idea is stable: it helps people discover public MCP servers.
It stores metadata, not source code. That metadata includes:
- the server name,
- the description,
- the version,
- how to find or install it, such as npm, PyPI, Docker, or a remote URL.
Names usually follow a reverse-DNS convention, such as io.github.ravi-dev/brightgrid. This helps keep names distinct and easier to manage.
Publishing
The basic publishing flow looks like this:
- Publish the package itself, such as to npm.
- Add the correct
mcpNameinpackage.json. - Create
server.jsonwithmcp-publisher init. - Log in with
mcp-publisher login github. - Publish with
mcp-publisher publish.
Check server.json before publishing
const pkg = {
name: "@ravi-dev/brightgrid-mcp",
version: "1.2.0",
mcpName: "io.github.ravi-dev/brightgrid",
};
const serverJson = {
name: "io.github.ravi-dev/brightgrid",
description: "Order lookups and returns for Brightgrid customers.",
version: "1.2.0",
packages: [
{
registryType: "npm",
identifier: "@ravi-dev/brightgrid-mcp",
version: "1.2.0",
transport: { type: "stdio" },
},
],
};
const checks = [
["name matches package.json mcpName", serverJson.name === pkg.mcpName],
[
"GitHub namespace io.github.<user>/",
/^io\.github\.[\w-]+\/[\w.-]+$/.test(serverJson.name),
],
["package identifier matches npm name", serverJson.packages[0].identifier === pkg.name],
[
"versions agree",
serverJson.version === pkg.version && serverJson.packages[0].version === pkg.version,
],
];
for (const [what, ok] of checks) {
console.log(ok ? "ok " : "FAIL", what);
}
Expected output:
ok name matches package.json mcpName
ok GitHub namespace io.github.<user>/
ok package identifier matches npm name
ok versions agree
The Registry is helpful for public servers, but not every server should be public. Internal tools can use a private registry or an internal catalog instead.
Common mistakes
- Testing only through the chat UI. If the model gives a wrong answer, you cannot tell whether the bad result happened in the tool, the schema, or the model itself. The Inspector reduces that confusion.
- Mixing SDK versions. Different package versions can make behavior hard to predict.
- Committing secrets. A
.envfile is not safe if it is in source control. - Publishing private servers to a public registry. Public discovery is for public products.
- Assuming every SDK supports the newest protocol everywhere. Check the changelog and release notes for each library and transport.
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- TypeScript SDK v2 gives you the core server and client packages; the Python equivalent is the
mcppackage. - The MCP Inspector is the main way to test a server before it is connected to a host or AI app.
- Keep a clean project layout with modules, tests, and placeholder config files.
- Put secrets in environment variables and validate them at startup.
- Pin SDK versions for the server and update intentionally.
- The MCP Registry is for public metadata discovery, not for private server code.
The main pattern is simple: use the tools that match the protocol, test early, verify configuration, and publish only when the server is ready.
Next: Host Configuration, where you learn how to connect your server to VS Code, Visual Studio, and other MCP hosts.