Developer Tool Servers
By the Syncfusion DevTutorials team · Last updated
Connecting development workflows through MCP: a design-system server for UI/UX and frontend, a read-only database server for backend work, repository resources, guards inside tools and clear tool descriptions.
Developers already juggle many systems: a design tool for screens and tokens, a repository, a database, a test runner, and a security scanner. An AI assistant that cannot see any of them has to guess, and guesses create hard-coded colors, invalid queries, and endpoints that ignore the real schema.
MCP servers give the assistant safe, structured access to those systems so it can work from the real design and the real data.
The main idea: developer tool servers expose the systems behind everyday development as MCP tools and resources. A design-system server gives the assistant tokens and screen designs for UI/UX and frontend work. A read-only database server gives it the real schema for backend work. A repository server exposes files as resources. Design these servers carefully: read-only by default, precise descriptions, guards inside the tools rather than only in prompts, and structured results the model and host can use.
A Real-World Example
Lina is building the login screen and its API at the same time. Her assistant is connected to the team's dev workbench server.
Frontend: she asks, "Build the login form from the design." The assistant calls get_frame("login-form") and get_design_tokens. It generates labelled inputs, a real <button> using var(--color-primary), and flags two problems in the design itself: the error text uses #FF0000 instead of the danger token, and there is no loading state.
Backend: she asks, "Add an endpoint to list a user's orders." The assistant calls describe_table("orders"), sees that user_id has no index, designs GET /users/{id}/orders with cursor pagination and a parameterized query, and suggests a migration to add the index. When it tries to check some data with DELETE FROM orders by mistake, the server refuses: it only runs a single SELECT.
In simple words: connect the assistant to the real design and the real database, and build the safety rules into the servers.
What You Will Learn
In this lesson, you will learn:
- Which developer systems make good MCP servers.
- How a design-system server supports UI/UX and frontend work.
- How a read-only database server supports backend work.
- How to put guards inside tools.
- How to describe developer tools so models use them well.
In the MCP Lab, pick the dev workbench server in the Explorer. It is a real MCP server written with the official SDK, like the help desk server, with eight tools, a repository resource template, and a prompt. The Server code tab still edits the help desk server, but every dev workbench call shows up in the Inspector, tagged [devtools].
Developer Systems as Servers
The best developer MCP servers expose the systems the assistant already needs to work with in real life. That gives the model access to the real design, real schema, and real code instead of making it guess.
| System | Tools and resources | Used for |
|---|---|---|
| Design system | get_design_tokens, get_frame | UI/UX reviews, building components that match the design |
| Database | describe_table, run_query (read-only) | Designing endpoints, writing queries, and migrations |
| Repository | repo://files/{path} resources, create_branch | Reading code and starting work |
| Test runner | run_tests | Checking changes (next lesson) |
| Security scanner | scan_secrets | Finding leaked keys (next lesson) |
Read the design tokens
Design and Frontend
From Design to Code
A design-system server gives the assistant the same source of truth designers and frontend developers use. That helps the model keep visual details consistent while it builds UI.
Generate markup from a frame and tokens
// Design to code: read a frame and the tokens, then generate component markup.
const tokens = {
"color.primary": "var(--color-primary)",
"color.danger": "var(--color-danger)",
};
const frame = {
name: "Login form",
elements: [
{ type: "input", label: "Email" },
{ type: "input", label: "Password", inputType: "password" },
{ type: "button", text: "Sign in", color: "color.primary" },
{ type: "text", text: "Wrong password", color: "#FF0000", state: "error" },
],
};
for (const el of frame.elements) {
if (el.type === "input") {
const id = el.label.toLowerCase();
console.log(
`<label for="${id}">${el.label}</label><input id="${id}"${el.inputType ? ` type="${el.inputType}"` : ""} />`,
);
} else if (el.type === "button") {
console.log(
`<button type="submit" style="background:${tokens[el.color]}">${el.text}</button>`,
);
} else {
const color =
tokens[el.color] ?? `${el.color} (not a token: use var(--color-danger))`;
console.log(`<p role="alert" style="color:${color}">${el.text}</p>`);
}
}
Expected output:
<label for="email">Email</label><input id="email" />
<label for="password">Password</label><input id="password" type="password" />
<button type="submit" style="background:var(--color-primary)">Sign in</button>
<p role="alert" style="color:#FF0000 (not a token: use var(--color-danger))">Wrong password</p>
Read the login form design
Review the Design Itself
The same data lets the assistant catch design problems before anyone writes code: missing labels, colors that are not tokens, and missing states.
Check a frame against the design system
// Check a design frame against the design system before coding it.
const tokenNames = ["color.primary", "color.text", "color.muted", "color.danger"];
const frame = {
elements: [
{ type: "input", label: "Email" },
{ type: "input" },
{ type: "button", text: "Sign in", color: "color.primary" },
{ type: "text", text: "Wrong password", color: "#FF0000", state: "error" },
],
states: ["default", "error"],
};
const issues = [];
frame.elements.forEach((el, i) => {
if (el.type === "input" && !el.label) {
issues.push(`element ${i}: input has no label`);
}
if (el.color && !tokenNames.includes(el.color)) {
issues.push(`element ${i}: color ${el.color} is not a token`);
}
});
if (!frame.states.includes("loading")) {
issues.push("frame: no loading state designed");
}
console.log(issues.join("\n"));
Expected output:
element 1: input has no label
element 3: color #FF0000 is not a token
frame: no loading state designed
Get the review_screen prompt
Return design data as structured content (JSON with token names, element types, and states), not as an image. Models reason about "color": "#FF0000" much more reliably than about pixels, and the host can render the structure for people too.
Backend
Schema-Aware Endpoints
A read-only database server lets the assistant design endpoints against the real schema instead of guessing column names. That is a big step up from "close enough" API design.
Design an endpoint from the schema
// Backend: design an endpoint from the real schema, read through a read-only tool.
const schema = [
{ column: "id", type: "integer", key: "primary" },
{ column: "user_id", type: "integer", key: "foreign users.id", indexed: false },
{ column: "total_cents", type: "integer" },
{ column: "created_at", type: "timestamptz", indexed: true },
];
const fields = schema.filter((c) => c.column !== "user_id").map((c) => c.column);
const notes = schema
.filter((c) => c.key?.startsWith("foreign") && !c.indexed)
.map((c) => `add an index on ${c.column}`);
console.log("GET /users/{id}/orders?cursor=&limit=");
console.log(`select: ${fields.join(", ")}`);
console.log("where: user_id = $1 AND created_at < $2 (parameterized)");
console.log(`migration needed: ${notes.join("; ") || "none"}`);
Expected output:
GET /users/{id}/orders?cursor=&limit=
select: id, total_cents, created_at
where: user_id = $1 AND created_at < $2 (parameterized)
migration needed: add an index on user_id
Describe the orders table
Guards Inside the Tool
"Only run SELECT queries" in a description is a request. A check in the tool's code is a rule. Put the guard where it cannot be talked around.
Guard a read-only query tool
// A read-only database tool must refuse anything but a single SELECT.
function guard(sql) {
if (!/^\s*select\b/i.test(sql)) {
return "REFUSED: not a SELECT";
}
if (/;\s*\S/.test(sql)) {
return "REFUSED: more than one statement";
}
if (/password_hash/i.test(sql)) {
return "REFUSED: sensitive column";
}
return "OK";
}
for (const q of [
"SELECT id, total_cents FROM orders WHERE user_id = 42",
"DELETE FROM orders",
"SELECT 1; DROP TABLE users",
"SELECT email, password_hash FROM users",
]) {
console.log(`${guard(q).padEnd(34)} ${q}`);
}
Expected output:
OK SELECT id, total_cents FROM orders WHERE user_id = 42
REFUSED: not a SELECT DELETE FROM orders
REFUSED: more than one statement SELECT 1; DROP TABLE users
REFUSED: sensitive column SELECT email, password_hash FROM users
Try a write query on a read-only tool
A read-only tool should also run with read-only credentials on a replica, so even a bug in the guard cannot change data. Defense in depth: the description tells the model, the guard stops mistakes, and the database permissions stop everything else.
Describing Developer Tools
Developer tools are only useful if the model picks the right one at the right time. A clear description helps the model make a correct choice and avoid dangerous actions.
Lint tool descriptions
// Good developer-tool descriptions say what, when, and what they never do.
const tools = [
{
name: "get_design_tokens",
description:
"Read the design system's color, spacing and radius tokens. Use before writing or reviewing UI.",
},
{
name: "run_query",
description:
"Run one SELECT query on the read-only replica. Other statements are refused.",
},
{ name: "do_db", description: "Database stuff." },
];
for (const t of tools) {
const hasUse = /use (it )?(before|when|after)/i.test(t.description);
const hasLimit = /read-only|refused|never|only/i.test(t.description);
const verdict = hasUse || hasLimit ? "ok" : "rewrite: say what, when, and limits";
console.log(`${t.name.padEnd(18)} ${verdict}`);
}
Expected output:
get_design_tokens ok
run_query ok
do_db rewrite: say what, when, and limits
| Part | Example |
|---|---|
| What it does | Read the columns, keys, and indexes of one table |
| When to use it | Use before designing an endpoint or query |
| Limits | In the read-only database; other statements are refused |
| Annotations | readOnlyHint: true, idempotentHint: true, openWorldHint: false |
Let the model pick a dev tool
Common mistakes
- Assistants that guess the design or the schema. Connect them to the real sources.
- Write access by default. Start with read-only tools, credentials, and replicas.
- Rules only in descriptions. Put guards in the tool code.
- Images instead of structure for design data.
- Vague tool descriptions like "Database stuff".
- Returning sensitive columns. Refuse or mask them in the tool.
Check Your Understanding
Exercises
Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.
What to Remember
- Developer tool servers connect assistants to the real design, database, and repository.
- A design-system server supports UI/UX reviews and frontend code that matches the design.
- A read-only database server supports schema-aware backend work.
- Enforce rules with guards in code and read-only credentials, not only descriptions.
- Return structured data and write descriptions that say what, when, and limits.
Next: Testing and Security Workflows, where MCP servers run tests and scan for secrets, and you learn to test and secure the servers themselves.