> ## Documentation Index
> Fetch the complete documentation index at: https://sdk.docs.xano.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AI agents and MCP servers

> Define tools, agents, MCP servers with prompts, resources and sign-in, and knowledge items that run inside your Xano backend, call an agent from an endpoint, and control what each connection sees.

These are AI features of the backend you ship: an agent your endpoint runs, or an MCP server
other clients connect to. They are not the coding agent that writes your code. For that, see
[Coding agents](/ai/coding-agents) and the Xano Engine's [MCP server](/run/engine-mcp).

Four kinds work together:

| Kind | What it is |
| - | - |
| `tool()` | An operation with typed input and a stack, which an agent or MCP server exposes. |
| `agent()` | An LLM with a system prompt and tools, run from a stack with `s.ai.agent.run`. |
| `mcpServer()` | Tools, prompts and resources served over MCP to outside clients. |
| `knowledge()` | Markdown that Xano's own AI reads before it acts. |

An MCP server also serves `prompt()` and `resource()` objects, and an agent or MCP server can
have a connection trigger (`agentTrigger`, `mcpServerTrigger`) that decides what each connection
sees.

## A tool

```ts theme={null}
// xano/ai/tools.ts
import { tool, input, inp, ref, s, cmp, col } from "@xano/sdk";
import { posts } from "../tables/blog.js";

export const searchPosts = tool({
  name: "search_posts",
  description: "Search posts by title",
  input: { query: input.text({ required: true }) },
  stack: [
    s.db.query({
      table: posts,
      where: cmp(col("title"), "includes", inp("query")), // case-insensitive substring
      as: "rows",
    }),
  ],
  response: ref("rows"),
});
```

## An agent, called from an endpoint

Agents have no public endpoint of their own. You run one from any stack:

```ts theme={null}
// xano/ai/assistant.ts
import { agent, query, input, inp, ref, obj, s, c } from "@xano/sdk";
import { blog } from "../api/blog.js";
import { searchPosts } from "./tools.js";

export const assistant = agent({
  name: "assistant",
  llm: {
    type: "xano-free", // needs no API key
    systemPrompt: "You answer questions about the blog.",
    prompt: "Answer this question: {{ $args.question }}",
    maxSteps: 5,
  },
  tools: [searchPosts],
});

export const ask = query({
  name: "ask",
  verb: "POST",
  apiGroup: blog,
  input: { question: input.text({ required: true }) },
  stack: [
    s.ai.agent.run({
      agent: assistant,
      args: obj({ question: inp("question") }), // becomes {{ $args }} in the prompt
      allowToolExecution: c.bool(true), // without it the agent sees its tools and never calls one
      as: "answer",
    }),
  ],
  response: { text: ref("answer.result") },
});
```

* **The answer is at `.result`.** `ref("answer")` is the whole run envelope (`finishReason`,
  `steps` and more).
* **`llm` picks the provider:** `anthropic`, `openai`, `google-genai` or `xano-free`, each with
  its own typed fields.
* **Structured output.** Give the agent `output: { schema: { ... } }` with `input.*` fields and
  `.result` is typed from it at every call site.
* **Prompts are templates.** String settings resolve `{{ $args.NAME }}` and
  `{{ $env.NAME }}` at run time.

### Control each connection: agent triggers

An `agentTrigger` runs when a client connects to the agent it is bound to. `t.toolset` is the
agent (`t.toolset("name")` reads one field) and `t.tools` is its list of tools, each
`{ id, name, instructions }`. What the trigger returns is the toolset and tools that connection
gets, so the trigger can gate the connection, or log and annotate it on the way through:

```ts theme={null}
// xano/ai/agent-trigger.ts
import { agentTrigger, s } from "@xano/sdk";
import { assistant } from "./assistant.js";

export const onAssistantConnect = agentTrigger({
  name: "on_assistant_connect",
  agent: assistant, // the agent() handle, resolved at export
  stack: (t) => [s.debug.log({ value: t.toolset("name") })],
  response: (t) => ({ toolset: t.toolset, tools: t.tools }), // the tools this connection gets
});
```

* **The stack is a callback.** A trigger has no `input`; its inputs arrive on `t`, typed to the
  trigger, so a wrong name is a compile error.
* **Bind with the handle.** Pass the `agent()` def, not an id. A trigger bound to nothing deploys
  and never fires.
* **Omit `response`** and the connection gets every tool unchanged.

## An MCP server

An MCP server exposes tools, prompts and resources to any MCP client, such as Claude, ChatGPT or
another agent. Prompts and resources are their own kinds: define them, then list them on the
server.

### A prompt

A prompt is a reusable message a client can ask for. Its `input` is the prompt's arguments; an
enum's values also answer the client's completion requests:

```ts theme={null}
// xano/ai/mcp.ts
import { prompt, input, inp, ref, s } from "@xano/sdk";
import { posts } from "../tables/blog.js";

export const summarizePost = prompt({
  name: "summarize_post",
  title: "Summarize a post",
  description: "Ask the model for a summary of one post",
  input: {
    post_id: input.int({ required: true, description: "The post to summarize" }),
    tone: input.enum(["neutral", "casual"], { description: "How the summary should read" }),
  },
  stack: [s.db.get({ table: posts, fieldValue: inp("post_id"), as: "post" })],
  response: ref("post.body"), // a text response is one user message
});
```

To send several messages, respond with a list of `{ role: "user" | "assistant", content }`.

### Resources

A resource is data a client reads by `uri`. A literal `uri` is a static resource. A `uri` with a
`{variable}` is a template: each variable is one `input` field, and a client reads a concrete
URI such as `blog://posts/42`.

```ts theme={null}
// xano/ai/mcp.ts (continued)
import { resource, input, inp, ref, s, c } from "@xano/sdk";
import { posts } from "../tables/blog.js";

export const styleGuide = resource({
  name: "style_guide",
  uri: "blog://style-guide",
  mimeType: "text/markdown",
  response: c.text("# Style guide\n\nShort sentences. One idea per paragraph."),
});

export const postById = resource({
  name: "post_by_id",
  uri: "blog://posts/{post_id}",
  title: "A post",
  mimeType: "application/json",
  input: { post_id: input.int({ required: true }) },
  stack: [s.db.get({ table: posts, fieldValue: inp("post_id"), as: "post" })],
  response: ref("post"),
});
```

### The server, with sign-in

List the tools, prompts and resources on the server. Add `oauth` to make every MCP request carry
a signed-in user from your auth table:

```ts theme={null}
// xano/ai/server.ts
import { mcpServer, env } from "@xano/sdk";
import { searchPosts } from "./tools.js";
import { summarizePost, styleGuide, postById } from "./mcp.js";
import { users } from "../tables/blog.js";

export const blogMcp = mcpServer({
  name: "blog_mcp",
  canonical: "blog-mcp",
  instructions: "Search, read and summarize the blog's posts.",
  tools: [{ tool: searchPosts, auth: users }],
  prompts: [summarizePost],
  resources: [styleGuide, { resource: postById, auth: users }],
  oauth: {
    mode: "hosted",
    authTable: users,
    loginUrl: env("MCP_LOGIN_URL"), // your sign-in page, set per environment
  },
});
```

* **Entries.** `tools`, `prompts` and `resources` each take handles, or a `{ tool | prompt |
  resource, enabled?, auth? }` wrapper. `auth` names the auth table and is what gives that
  entry's stack a caller for `auth()`. A bare handle is public.
* **`blogMcp.getUrl(HOST)`** returns the Streamable HTTP URL a client connects to.
* **Names.** An `mcpServer` and an `agent` with the same name collide, because both are stored
  as toolsets.

#### Hosted sign-in

With `mode: "hosted"`, Xano is the authorization server. A client that connects is sent to your
`loginUrl` with an `mcp_request` parameter. That page is also the consent screen: it shows who is
asking, signs the user in, then calls an endpoint that completes the sign-in and redirects the
browser to the URL it returns. Two endpoints back the page:

```ts theme={null}
import { apiGroup, query, input, inp, ref, s } from "@xano/sdk";
import { users } from "../tables/blog.js";

export const mcpSignIn = apiGroup({ name: "mcp_sign_in", canonical: "mcp-sign-in" });

// The page shows these before the user approves: client_name, client_logo, callback_host,
// client_verified, server_name.
export const signInDetails = query({
  name: "mcp/request",
  verb: "GET",
  apiGroup: mcpSignIn,
  input: { mcp_request: input.text({ required: true }) },
  stack: [s.mcp.oauth.request({ request: inp("mcp_request"), as: "sign_in" })],
  response: ref("sign_in"),
});

// Called once the user has signed in and chosen. Returns the URL to redirect to.
export const completeSignIn = query({
  name: "mcp/complete",
  verb: "POST",
  apiGroup: mcpSignIn,
  auth: users, // the signed-in user is the one the MCP client acts as
  input: {
    mcp_request: input.text({ required: true }),
    decision: input.enum(["approve", "deny"], { required: true }),
  },
  stack: [s.mcp.oauth.complete({ request: inp("mcp_request"), decision: inp("decision"), as: "continue_url" })],
  response: ref("continue_url"),
});
```

Declare `MCP_LOGIN_URL` in `workspaceConfig({ env: { MCP_LOGIN_URL: "" } })` and set its value
per environment in `xano/.env` (see [Env and secrets](/build/env-and-secrets)).
`s.mcp.oauth.revoke` ends a user's sign-ins on a server.

#### Your own identity provider

To accept tokens from Auth0, Clerk, WorkOS, Entra, Okta or Cognito instead, use
`mode: "external"`. The token's claim (default `sub`) is matched against a column in your auth
table, here an `auth0_id` text column on `users` that holds each user's provider id:

```ts theme={null}
oauth: {
  mode: "external",
  authTable: users,
  issuer: env("OAUTH_ISSUER"),
  column: "auth0_id",
  preset: "auth0",
},
```

### Control each connection: MCP server triggers

An `mcpServerTrigger` runs when an MCP client connects to the server it is bound to, and its
response decides what that one connection lists. `t` carries the server's `toolset`, `tools`,
`prompts` and `resources`. Return a list filtered to narrow it; a key you leave out lists
everything the server has. This one hides the resources from every connection:

```ts theme={null}
// xano/ai/mcp-trigger.ts
import { mcpServerTrigger, c } from "@xano/sdk";
import { blogMcp } from "./server.js";

export const onBlogMcpConnect = mcpServerTrigger({
  name: "on_blog_mcp_connect",
  mcpServer: blogMcp,
  response: (t) => ({
    toolset: t.toolset,
    tools: t.tools,
    prompts: t.prompts,
    resources: c.array([]), // this connection lists no resources
  }),
});
```

* **`t.auth` is the signed-in user** when the server has `oauth`: `{ dbo, id, extras }` of the
  auth-table row, read as `t.auth("id")`. It is null on a server without sign-in, so check it
  before reading a field.
* **An entry you return can change its description.** A returned prompt or resource's
  `description` replaces the listed one for that connection.
* **Omit `response`** and the connection lists all four unchanged.

## Knowledge

A `knowledge()` item is a markdown file that Xano's own AI reads before it acts: the
workspace's builder agent, and outside agents that read the workspace through the Meta API or
MCP. It does not reach an `agent()` you run with `s.ai.agent.run`. Put what that agent must know
in its `llm.systemPrompt`.

```ts theme={null}
import { knowledge, knowledgeFile } from "@xano/sdk";

export const deployRunbook = knowledge({
  name: "deploy_runbook",
  description: "How this workspace ships: environments, deploy gates, and rollback.",
  type: "skill",
  mode: "auto", // only the name and description are sent until a request matches
  body: knowledgeFile("./knowledge/deploy-runbook.md", import.meta.url),
});
```

`mode: "always"` sends the whole body on every request, and a `type: "agents.md"` item is
always sent in full. Write the `description` of an `auto` item to match the requests it should
answer.

## Register them

```ts theme={null}
export default workspace("my-app")
  // ...tables, API groups, queries
  .registerTools([searchPosts])
  .registerAgents([assistant])
  .registerPrompts([summarizePost])
  .registerResources([styleGuide, postById])
  .registerMcpServers([blogMcp])
  .registerApiGroups([mcpSignIn])
  .registerQueries([signInDetails, completeSignIn])
  .registerTriggers([onAssistantConnect, onBlogMcpConnect])
  .registerKnowledge([deployRunbook]);
```

## Next

* Every `llm` field, MCP prompts and resources, elicitation and background runs:
  [object kinds reference](/reference/object-kinds).
* Run work on a schedule or on Xano platform events: [Tasks and platform events](/build/tasks).
* Call code the typed surface cannot express, from a tool or anywhere else:
  [Custom code](/build/custom-code).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.