# MCP: Tools from Outside the App

Course: [Building AI Agents](https://www.devtutorials.ai/learn/building-ai-agents) · Section: Agents in a Real Product
Source: https://www.devtutorials.ai/learn/building-ai-agents/production/mcp-tools

> MCP lets an AI helper plug in tools from other services. Learn how an app connects to an MCP server, gets its tool cards, checks who may use them, and copes when the server stops answering.

## MCP: Tools from Outside the App

Asha asks Shop Helper where her parcel is.

Shop Helper calls the courier's tracker. The tracker does not answer.

The helper does not say so. It writes, "Your order is on its way and should arrive soon."

Nobody checked that. Asha waits at home all day for nothing.

The shop added the tracker in a hurry. Nobody planned for the day it would go quiet.

This lesson shows how a helper plugs in tools from other services. You will also see how to keep those tools safe, and what to do when they stop answering.

**Simple lesson:** a tool from outside is useful, but the app still owns what the helper says.

### A Real-World Example

Think about the power sockets in your home.

- You buy a new lamp. You plug it in, and it works.
- You do not rewire the house for each new lamp.
- The socket is the same shape for every device.
- A fuse still protects the house, and a plug can still fail.

Tools from other services work in a similar way. The app has one kind of socket, and each service brings its own plug.

**Simple lesson:** one shared socket beats a new wire for every device.

## What You Will Learn

In this lesson, you will learn:

1. Why tools wired into the app by hand break easily.
2. What MCP is, and how an app connects to an MCP server.
3. How the helper gets and uses tool cards from outside.
4. How the app limits each outside tool to the right customer.
5. What the helper should do when an outside server stops answering.

## Hand-Wired Tools

Shop Helper's six tools live inside the shop's own app. The shop wrote them, so it controls them.

Now the shop wants parcel tracking. The courier, **Speedy Parcels**, knows where every van is. The shop does not.

The quick way is to write tracker code by hand inside the app. It works on day one.

But the courier owns the other side. When the courier changes its tool, the shop's code breaks, because it still sends the old input.

Press **Play** and watch the cord between the app and the courier.

- **Hand-wired:** the shop's code sends the input name it was written for.
- **The change:** the courier renames that input, and the call fails.
- **Plugged in:** the app asks the courier for its current tool cards first.
- **The result:** the same courier change, but this time the answer is right.

MCP does not make every change safe. The model reads the new card, so a small change like a renamed input can keep working. Bigger changes can still break things, so test after any server update.

**Simple lesson:** code that copies someone else's tool breaks when they change it.

## What MCP Is

**MCP** = the Model Context Protocol, a shared way for apps to offer tools to AI helpers.

Think of it as the socket shape. Any service that follows it can plug into any app that follows it.

**MCP server** = a program run by a service, such as a courier, that offers its tools through MCP.

An MCP server can also run on your own machine, started by your app. It does not have to be a remote service.

**Connect** = the app opens a line to an MCP server and asks for its list of tools.

The server answers with tool cards. You met tool cards in **Tool Design**. Each one has a name, a description and inputs.

Here is a short tool list answer from the courier. It is simplified. Real answers often hold more detail, but the main parts look like this.

```text
{
  "tools": [
    {
      "name": "track_parcel",
      "description": "Track a Speedy Parcels parcel. Returns where it is and its delivery day.",
      "inputs": { "order_id": "4-digit order number, like 1042" }
    },
    {
      "name": "delivery_slots",
      "description": "List free delivery slots for a parcel not delivered yet.",
      "inputs": { "order_id": "4-digit order number, like 1042" }
    }
  ]
}
```

The courier wrote these cards, not the shop. When the courier changes a tool, it changes the card too.

So outside tool cards are text from someone else. Read them before you connect. Notice when they change, because a changed description could steer the model (rule 4) or break what you expect. MCP lets a server tell the app when its tool list changes.

Press **Play** and watch the tool shelf.

- **Before:** the shelf holds the shop's own six tools.
- **Connect:** the plug goes in, and the app asks for the list.
- **The cards:** two orange cards slide onto the shelf.
- **One shelf:** outside tools sit next to the shop's own tools.

**Using MCP, not building it**

MCP has more parts than tools, such as ways to share files and prompts.

To learn how MCP works inside, and how to build your own server, take the **Model Context Protocol** course.

**Simple lesson:** connect once, ask for the list, and the cards come from the service.

## The Helper Uses an Outside Tool

Once the cards are on the shelf, the model treats them like any other tool.

The pretend model picks the card whose description best matches what the customer needs. It does not care where a tool came from.

When it asks for an outside tool, the app sends the call to the MCP server. The server does the work and sends a result back.

Press **Play** and follow the call.

- **The pick:** the words "parcel" and "now" match the courier's card.
- **The call:** the app sends the order number to the courier.
- **The result:** the courier says where the van is and the delivery day.
- **The reply:** Asha sees one helper, not two companies.

An outside result is still just text from someone else. It may hold hidden orders aimed at the AI. Mark it as **just data**, as you saw in **Guardrails, Permissions and Human Approval**.

**Simple lesson:** outside tools are picked and used like your own, and their results are only data.

## Who May Use Which Tool

Plugging in a tool does not mean every customer should get it.

The courier's server can see every parcel it carries. If the app calls it with the shop's master key, any customer could ask about any parcel.

**Tool scope per user** = the outside tools each customer may use, and the data each tool may show them.

In this lab, the shop has two scope rules:

- **Which tools:** only customers who linked a Speedy account get delivery_slots. Asha linked hers. Ravi did not.
- **Which parcels:** the app tells the server who is chatting, so it only shows that customer's parcels.

Press **Play** and compare the two windows.

- **No scope:** Asha sees Ravi's parcel, and Ravi uses a tool he never got.
- **Nothing crashed:** both tools worked, so nobody would notice the leak.
- **Scope on:** the server refuses Asha, and delivery_slots never reaches Ravi's shelf.

**The app is still in charge**

An outside tool runs on someone else's computer. The permission to use it is still yours to give.

Check outside tools with the same rules as your own. MCP supports passing each user's own login (OAuth) to a remote server. Use that when the server holds per-user data, not one master key.

**Simple lesson:** each customer gets only the outside tools and data that are theirs.

## When the Plug Goes Dark

Outside servers live on other computers. They can be slow, or stop answering in the middle of a chat.

The pretend model has a weak spot here. When a tool gives it nothing useful, it may make something up.

So the app needs a plan. A good plan has three steps:

- **Retry within a limit:** a short outage often passes after a second try.
- **Then be honest:** say the tracker is down, and offer to check later.
- **Or pass it on:** when the customer needs something done, hand over to a person with a note.

The helper must never fill the gap with a guess, because a made-up date looks just like a real one.

Press **Play** and watch the light on the courier's plug.

- **Made up:** with no plan, the helper invents a cheerful answer.
- **Retry:** the next helper tries again, and the server is back on try 3.
- **Long outage:** the server stays down, so it stops after 3 tries and tells the truth.
- **The wait:** every try that gets no answer adds 4 seconds.

**Pick the retry limit on purpose**

- Too few retries, and short hiccups become hand-overs.
- Too many, and the customer stares at a spinner when the server is truly gone.
- Choose a limit that fixes short outages and still answers within a fair wait.

In this lab, each try waits a fixed 4 seconds. Real apps often wait a bit longer each time (backoff), as in **Observability, Cost and Reliability**.

A retried action on an outside server has the same "safe to retry?" question. A slot booking may have gone through even if the answer never came back. Check first, or send a request ID.

**Simple lesson:** retry a few times, then tell the truth, but never guess.

## Where This Is Used

Many AI apps now plug in outside tools through MCP.

- **Support helpers** connect to couriers, payment services and ticket systems.
- **Coding helpers** connect to code hosts, issue trackers and databases.
- **Office helpers** connect to calendars, email and shared drives.

The same lessons apply each time. Read the cards, set the scope, and plan for the day the server goes quiet.

**Simple lesson:** every new plug brings new tools, and new ways to fail.

## In Code

Here is the client side of MCP inside the shop's app. It uses the same Shop Helper names: `lookup_order`, `track_parcel`, order 1044 and Asha's customer ID C-101.

The courier server below is a mock object. It has the two calls an MCP client usually makes: list the tools, and call one.

```ts runnable title="Merge, scope and retry an outside server"
interface ToolCard { name: string; description: string; source: "shop" | "speedy"; }
interface ToolResult { text: string; isError: boolean; }
interface McpLikeServer {
  listTools(): ToolCard[];
  callTool(name: string, args: { order_id: number }, user: string): ToolResult;
}

// Mock courier server. It is down for the first `downFor` calls.
function makeSpeedy(downFor: number): McpLikeServer {
  let calls = 0;
  const owners: Record<number, string> = { 1042: "C-101", 1044: "C-101", 1043: "C-102" };
  return {
    listTools: () => [
      { name: "track_parcel", description: "Track a Speedy parcel.", source: "speedy" },
      { name: "delivery_slots", description: "List free delivery slots.", source: "speedy" },
    ],
    callTool(name, args, user) {
      calls++;
      if (calls <= downFor) throw new Error("no answer");
      if (owners[args.order_id] !== user) return { text: "not your parcel", isError: true };
      return { text: `${name}: order ${args.order_id} arrives Monday 3 Nov`, isError: false };
    },
  };
}

const shopTools: ToolCard[] = [
  { name: "lookup_order", description: "Look up an order.", source: "shop" },
  { name: "issue_refund", description: "Refund an order.", source: "shop" },
];

// Scope per user: only linked Speedy accounts get delivery_slots.
const linked: Record<string, boolean> = { "C-101": true, "C-102": false };
function toolsFor(user: string, server: McpLikeServer): string[] {
  const outside = server.listTools().filter((t) => t.name !== "delivery_slots" || linked[user]);
  return shopTools.concat(outside).map((t) => t.name);
}

function callWithRetry(server: McpLikeServer, name: string, order: number, user: string, maxTries: number): string {
  let waited = 0;
  for (let tryNo = 1; tryNo <= maxTries; tryNo++) {
    try {
      const r = server.callTool(name, { order_id: order }, user);
      return `try ${tryNo}: ${r.isError ? "refused, " : ""}${r.text}`;
    } catch {
      waited += 4; // pretend each silent try costs 4 seconds
    }
  }
  return `gave up after ${maxTries} tries (${waited}s): "The tracker is down. I can check again later."`;
}

console.log("Asha:", toolsFor("C-101", makeSpeedy(0)).join(", "));
console.log("Ravi:", toolsFor("C-102", makeSpeedy(0)).join(", "));
console.log(callWithRetry(makeSpeedy(2), "track_parcel", 1044, "C-101", 3));
console.log(callWithRetry(makeSpeedy(0), "track_parcel", 1043, "C-101", 3));
console.log(callWithRetry(makeSpeedy(9), "track_parcel", 1044, "C-101", 3));
```

Expected output:

```text
Asha: lookup_order, issue_refund, track_parcel, delivery_slots
Ravi: lookup_order, issue_refund, track_parcel
try 3: track_parcel: order 1044 arrives Monday 3 Nov
try 1: refused, not your parcel
gave up after 3 tries (12s): "The tracker is down. I can check again later."
```

- **Merge:** `toolsFor` puts the courier's cards next to the shop's own cards, so the model sees one list.
- **Scope:** Ravi has no linked Speedy account, so `delivery_slots` never reaches his list. The server also refuses Asha when she asks about Ravi's order 1043.
- **Retry:** the first run gets an answer on try 3. The last run stays down, so the helper stops after 3 tries and says so honestly.
- **No guess:** the give-up message never invents a delivery day.

### In a Real App

A real app uses an MCP client SDK. The sketch below uses the official TypeScript SDK's client package, as its docs show it. It is simplified. Package names and options change between SDK versions, so check the version you install.

```ts
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";

// One client per server. Connect once when the app starts.
const client = new Client({ name: "shop-helper", version: "1.0.0" });
await client.connect(
  new StreamableHTTPClientTransport(new URL("https://mcp.speedy.example/mcp"))
);

// List the courier's tools. Follow nextCursor if the list comes in pages.
const { tools } = await client.listTools();

// Merge them with the shop's own tool cards, after the per-user scope filter.
const allowed = tools.filter((t) => t.name !== "delivery_slots" || user.speedyLinked);
const toolList = [...shopTools, ...allowed.map((t) => ({
  name: t.name,
  description: t.description ?? "",
  input_schema: t.inputSchema,
}))];

// When the model asks for an outside tool, send the call to the server.
const result = await client.callTool({
  name: "track_parcel",
  arguments: { order_id: 1044 },
});
// A tool can fail in two ways: it throws, or it returns isError: true. Check both.
if (result.isError) {
  // pass the error text back to the model, marked as just data
}

await client.close();
```

The real call can throw when the server stops answering. Wrap it in the same retry loop as the runnable cell. To send each customer's own login to the server, the HTTP transport accepts an auth provider. The **Model Context Protocol** course covers the protocol, servers and auth in depth.

**Simple lesson:** list the outside tools, filter them per customer, and retry each call with an honest way out.

## TL;DR

1. **MCP** = Model Context Protocol, a shared way for apps to offer tools to AI helpers.
2. **MCP server** = a program a service runs to offer its tools through MCP.
3. **Connect** = the app opens a line to the server and asks for its tool list.
4. **Outside tool card** = a name, a description and inputs, written by the service.
5. **Tool scope per user** = which outside tools each customer may use, and which data they may see.
6. **Server down** = retry within a limit, then an honest message or a hand-off, never a guess.
7. **Outside results** = data, not instructions.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Hand-wiring every outside tool | "Copying the courier's tool into our code is quicker." | Connect through MCP and read the current cards. | The code breaks when the courier changes its tool. |
| One master key for all customers | "The server only answers what the helper asks." | Pass on who is chatting, and limit tools per customer. | One customer sees another customer's parcel. |
| No plan for a dead server | "The courier is always up." | Retry within a limit, then be honest or hand over. | The helper makes up a delivery date. |
| Retrying forever | "One more try will work." | Stop at a small limit and say what happened. | The customer waits and the coins run out. |
| Trusting outside results as orders | "It came from our partner, so it is safe." | Mark outside results as just data. | A hidden line in a result steers the helper. |

## Check Your Understanding

## Exercises

## What to Remember

- **MCP** lets the helper plug in tools from other services, like a device in a socket.
- The app **connects** to an **MCP server** and asks for its tool cards.
- The model picks outside tools by their cards, just like its own.
- Set **tool scope per user**: the right tools, and only the customer's own data.
- When a server stops answering, retry within a limit, then be honest or hand over.
- Outside results are data, not instructions.

Next: **Evaluating Agents**, how to test the helper with many sample chats every time you change it.
