Host Experience Design

By the Syncfusion DevTutorials team · Last updated

Designing the user experience of MCP hosts: approval policies from tool annotations, approval dialogs, remembered decisions, accessible elicitation forms, progress and cancellation, errors and multi-server display.

MCP gives an AI app real powers: create a branch, run the tests, query a database, or delete a file. Whether people trust those powers depends almost entirely on the host's user experience: what the app shows before a tool runs, how it asks for permission, how it gathers missing information, and how it reports progress and errors.

A protocol can describe a tool as read-only or destructive, but only the host can turn that into a dialog a person can understand and trust.

The main idea: the host owns the user experience of MCP. Start with a sensible approval policy: usually, ask before anything that changes data. Show a clear approval dialog with the real arguments and the server's hints, never offer "Always allow" for destructive tools, and remember choices per server and tool. Turn elicitation schemas into accessible forms. Show progress with a working Stop control, explain errors in plain language, and make it obvious which server each tool comes from.

A Real-World Example

A team connects its coding assistant to a dev workbench MCP server with tools for design tokens, tests, the database, and git. The first host version calls every tool silently. One afternoon, a vague request like "clean up the old branches" makes the model call delete_branch four times. Two of those branches still have unmerged work.

The team redesigns the host:

  1. Read-only tools such as token and test lookups run without asking, and they appear in the activity panel.
  2. Write tools such as create_branch show a dialog with the arguments and offer Allow once, Always allow, or Deny.
  3. Destructive tools such as delete_branch are highlighted in red, show exactly which branch will be removed, and only offer Allow once or Deny.
  4. A Host UX settings page lets each developer review and revoke saved "Always allow" decisions.

Nothing about the server changed. The interface made it safe.

In simple words: the host decides what people see and approve. Make every tool call understandable, and every risky call deliberate.

What You Will Learn

In this lesson, you will learn:

  1. How to choose an approval policy from tool annotations.
  2. What a good approval dialog should show.
  3. How to remember decisions safely.
  4. How to design accessible elicitation forms.
  5. How to show progress, cancellation, and errors.
  6. How to present several servers clearly.

This chapter, Development Workflows, uses a new lab server, the dev workbench, with developer tools for design tokens and frames (UI/UX and frontend), a read-only database (backend), a test runner (testing), a secret scanner (security), and git branches. The MCP Lab also has a new Host UX tab where you choose an approval policy and review every decision.

Approval Policies

Every approval policy starts with understanding the tool itself. MCP tools include annotations that help the host decide whether a call can run automatically or should require the user's approval.

Start with the Annotations

Tools carry annotations such as readOnlyHint and destructiveHint (see Tools). The host turns those hints into a policy for when it should ask the user:

Which calls need approval?

// Approval policy: which tool calls need the user's OK?
function needsApproval(tool, policy) {
  if (policy === "all") {
    return true;
  }
  if (policy === "never") {
    return false;
  }
  return tool.annotations.readOnlyHint !== true; // "writes": ask for anything that may change data
}

const tools = [
  { name: "get_design_tokens", annotations: { readOnlyHint: true } },
  { name: "run_tests", annotations: { readOnlyHint: true } },
  { name: "create_branch", annotations: { readOnlyHint: false, destructiveHint: false } },
  { name: "delete_branch", annotations: { readOnlyHint: false, destructiveHint: true } },
  { name: "mystery_tool", annotations: {} },
];
for (const t of tools) {
  console.log(
    `${t.name.padEnd(18)} writes-policy: ${needsApproval(t, "writes") ? "ASK" : "auto"}`,
  );
}

Expected output:

get_design_tokens  writes-policy: auto
run_tests          writes-policy: auto
create_branch      writes-policy: ASK
delete_branch      writes-policy: ASK
mystery_tool       writes-policy: ASK
PolicyBehaviorWhen it fits
Ask for tools that change dataRead-only tools run; others askThe usual default
Ask for every toolEvery call asksUntrusted or new servers
Never askEvery call runsOnly in throwaway sandboxes

Treat a tool with no annotations as "may change data." The hints come from the server, so they are hints, not guarantees. A host should trust them only as much as it trusts the server.

Choose an approval policy

Call a write tool on the dev workbench

The approval dialog appears before anything reaches the server. Press Deny and look at the Inspector: no tools/call message was sent.

Approval Dialogs

What to Show

A clear approval dialog gives the user the information needed to make a real decision. The core question is not "Did the assistant use a tool?" It is "What exactly will happen if I approve this?"

Build the dialog text from tool metadata

// What the approval dialog should show, built from the tool's metadata.
function dialog(server, tool, args) {
  const a = tool.annotations;
  const lines = [
    `Allow "${tool.title}" from ${server}?`,
    `Arguments: ${JSON.stringify(args)}`,
  ];
  lines.push(a.readOnlyHint ? "Read-only" : "Changes data");
  if (a.destructiveHint) {
    lines.push("Destructive: cannot be undone");
  }
  const buttons = a.destructiveHint
    ? ["Allow once", "Deny"]
    : ["Allow once", "Always allow", "Deny"];
  lines.push(`Buttons: ${buttons.join(" | ")}`);
  return lines.join("\n");
}

console.log(
  dialog(
    "dev workbench",
    {
      title: "Delete a git branch",
      annotations: { readOnlyHint: false, destructiveHint: true },
    },
    { name: "feat/old-login" },
  ),
);

Expected output:

Allow "Delete a git branch" from dev workbench?
Arguments: {"name":"feat/old-login"}
Changes data
Destructive: cannot be undone
Buttons: Allow once | Deny

A good dialog shows:

  • Who: the tool's title and the server's name.
  • What: the exact arguments, readable and not hidden or summarized away.
  • Risk: read-only, changes data, destructive, or reaches outside systems.
  • Choices: Allow once, Always allow (not for destructive tools), or Deny.

See a destructive tool's dialog

Never show a dialog that says "The assistant wants to use a tool" with an OK button. People click OK on anything they do not understand. The arguments are the most important part: "Delete branch feat/old-login" is a decision someone can make; "Run delete_branch" is not.

Remembering Decisions

Saved approvals make tools easier to use, but they also affect future tool calls. Hosts need to balance convenience with safety.

"Always allow" saves clicks, but it must be scoped and visible. A host should not forget that saved approvals affect trust and safety.

Scope remembered approvals

// "Always allow" memory: scope it to server + tool, never to destructive tools.
const always = new Set();

function decide(server, tool, destructive, choice) {
  const key = `${server}/${tool}`;
  if (always.has(key) && !destructive) {
    return "auto-allowed (remembered)";
  }
  if (choice === "always" && !destructive) {
    always.add(key);
  }
  return choice === "deny" ? "denied" : "allowed";
}

console.log(decide("devtools", "create_branch", false, "always"));
console.log(decide("devtools", "create_branch", false, "deny"));
console.log(decide("other-server", "create_branch", false, "once"));
console.log(decide("devtools", "delete_branch", true, "always"));
console.log(decide("devtools", "delete_branch", true, "once"));

Expected output:

allowed
auto-allowed (remembered)
allowed
allowed
allowed
  • Scope the decision to server + tool, not to a whole server and not across servers that happen to use the same tool name.
  • Never remember approvals for destructive tools.
  • Show remembered approvals in settings, with a Revoke button.
  • Reset them when a server's tools change in a way that matters, such as a new version or new annotations.

Review and revoke decisions

Elicitation Forms

Sometimes a tool cannot continue until the user provides additional information. Instead of guessing, the host collects that information through an elicitation form.

When a server needs some information during a tool call, it sends an elicitation request with a schema (see Elicitation). The host then turns that schema into a form the user can complete.

Schema fields to accessible controls

// Elicitation form UX: turn the server's schema into accessible form fields.
const schema = {
  properties: {
    branch: {
      type: "string",
      title: "Branch name",
      description: "Lowercase, like feat/login",
    },
    notify: { type: "boolean", title: "Notify the team" },
    reviewer: { type: "string", enum: ["dana", "ravi", "mei"], title: "Reviewer" },
  },
  required: ["branch"],
};

for (const [key, f] of Object.entries(schema.properties)) {
  const control = f.enum
    ? `select (${f.enum.join("/")})`
    : f.type === "boolean"
      ? "checkbox"
      : "text input";
  const req = schema.required.includes(key) ? " *required" : "";
  console.log(
    `<label for="${key}">${f.title}${req}</label> -> ${control}${f.description ? ` · hint: ${f.description}` : ""}`,
  );
}

Expected output:

<label for="branch">Branch name *required</label> -> text input · hint: Lowercase, like feat/login
<label for="notify">Notify the team</label> -> checkbox
<label for="reviewer">Reviewer</label> -> select (dana/ravi/mei)
  • Every field gets a visible label, and required fields are marked.
  • Enums become selects or radio buttons; booleans become checkboxes.
  • Show the server's name and message so people know who is asking.
  • Offer Decline and Cancel as clearly as Submit.
  • Never use elicitation forms for passwords or secrets. Use the URL mode to send people to a trusted page instead.

Progress, Cancel, and Errors

Long tools such as test runs, exports, and migrations should feel alive and stoppable. A user should always know whether a task is working, how far along it is, and how to stop it.

Show progress and a Stop button

// Progress and cancel: show progress messages and let the user stop a long tool.
const updates = [
  { progress: 1, total: 4, message: "Installing dependencies" },
  { progress: 2, total: 4, message: "Running unit tests" },
  { progress: 3, total: 4, message: "Running integration tests" },
];
for (const u of updates) {
  const pct = Math.round((u.progress / u.total) * 100);
  console.log(
    `[${"#".repeat(pct / 10).padEnd(10)}] ${String(pct).padStart(3)}% ${u.message}`,
  );
}
console.log("User pressed Stop -> client aborts the request -> server stops the work");

Expected output:

[##        ]  25% Installing dependencies
[#####     ]  50% Running unit tests
[#######   ]  75% Running integration tests
User pressed Stop -> client aborts the request -> server stops the work
  • Show progress notifications as a bar or a list of steps.
  • A Stop button aborts the request, and the server should stop its work.
  • Errors should say what failed and what to do next: "The test runner timed out after 60 seconds. Try running a smaller filter." Not "Error -32603".

Watch progress, then press Stop

Several Servers

When a host connects several servers, people need to know which one did what:

  • Show the server name next to every tool call in the activity list.
  • Use prefixed names internally (for example, devtools__run_tests) to avoid collisions, but show friendly titles to people.
  • Let people disconnect a server or turn off individual tools.

Add an activity panel: a simple list of every tool call in the conversation, with the server, tool, arguments, result status, and the approval decision. It answers "what did the assistant just do?" in one glance, and it is usually the first place people look when something unexpected happens.

Chat with the dev workbench connected

Common mistakes

  • Silent write tools. Ask before anything that changes data.
  • Dialogs without arguments. Show exactly what will happen.
  • "Always allow" for destructive tools. Offer Allow once or Deny only.
  • Unscoped or invisible remembered approvals. Scope to server + tool, and let people revoke them.
  • Unlabelled elicitation forms, or forms that ask for secrets.
  • Progress with no Stop, and errors shown as raw codes.

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

  • The host owns the UX of MCP: approvals, forms, progress, and errors.
  • The default policy is ask before tools that change data; treat missing hints as unsafe.
  • Dialogs show who, what (exact arguments), risk, and choices.
  • Remember approvals per server + tool, never for destructive tools, and let people revoke them.
  • Elicitation forms are labelled and accessible and should never ask for secrets.
  • Long tools show progress and a working Stop button; errors are written for people.

Next: Developer Tool Servers, where you connect design, frontend, and backend workflows through MCP.