# Developer Tool Servers

Course: [Model Context Protocol](https://www.devtutorials.ai/learn/mcp) · Section: Development Workflows
Source: https://www.devtutorials.ai/learn/mcp/dev-workflows/developer-tool-servers

> 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:

1. Which developer systems make good MCP servers.
2. How a design-system server supports UI/UX and frontend work.
3. How a read-only database server supports backend work.
4. How to put guards inside tools.
5. 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) |

## 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.

```js runnable title="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:

```text
<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>
```

### 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.

```js runnable title="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:

```text
element 1: input has no label
element 3: color #FF0000 is not a token
frame: no loading state designed
```

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.

```js runnable title="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:

```text
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
```

### 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.

```js runnable title="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:

```text
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
```

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.

```js runnable title="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:

```text
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` |

## 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.
