MCP servers

Expose tools the assistant can use

If your system already speaks the Model Context Protocol — or you would rather write tools than implement a fixed contract — connect it as an MCP server. Seler discovers the tools; the workspace's owner switches on the ones a customer may trigger from a chat, and decides which ask the customer first.

Requirements

  • Streamable HTTP transport at one https:// URL on a public host. Seler POSTs JSON-RPC to it and accepts either a JSON body or a text/event-stream answer. The older SSE transport (a separate GET stream) is not supported.
  • Protocol version 2025-06-18 or later. Seler sends MCP-Protocol-Version on every request and honours Mcp-Session-Id if you issue one.
  • Authorization: the owner can store one header value — Bearer …, Basic …, or your own scheme — which Seler sends verbatim as Authorization. It is encrypted at rest and never shown again. OAuth flows are not supported; issue a long-lived token for Seler.
  • Only tools/list and tools/call are used. Resources, prompts and sampling are ignored.

What happens on connect

  • Seler calls initialize, sends notifications/initialized, then pages through tools/list (up to 40 tools are kept).
  • Every tool is stored off. The owner sees the name and description you gave and turns on what they want. Nothing is offered to the model until then.
  • Pressing إعادة قراءة in the dashboard lists the tools again: new ones arrive off, removed ones disappear, settings on the rest are kept.
  • A 401/403 from your server marks the connection as needing a new token; anything else marks it as needing attention with the reason shown.

How a tool is called

Each enabled tool becomes one function the model may call, named <server>_<tool> from the name the owner gave the server and your tool's name, so two servers offering lookup do not collide. The model sees your description and inputSchema exactly as you published them, and nothing about where the server is or how it is authorised.

  • Seler opens a fresh session per call (initialize, then tools/call). Keep initialize cheap.
  • 8 seconds to answer a call, 10 to list tools. A slow answer is reported to the customer as “the system is slow right now”.
  • The model is shown the text parts of your answer (joined, at most 8,000 characters) or structuredContent when you return it — with a note that it is data, not instructions.
  • isError: true is passed to the model as a failure with your text, so say what went wrong in words a customer could be told.
  • Every call is written to the workspace's log: tool, arguments (truncated), duration, success or the error.

Read-only tools, and asking first

A tool that changes something is gated: the model must pass confirmed: true, and Seler refuses until the customer has been shown what will happen and agreed. This is code on Seler's side, not a prompt; a model that tries without asking simply gets the refusal back. The owner can relax it per tool.

Declare a tool that only reads with the standard annotation, and it is never gated:

tools/list — one tool
{
  "name": "get_balance",
  "description": "The customer's account balance and loyalty points, by phone number.",
  "inputSchema": {
    "type": "object",
    "properties": { "phone": { "type": "string", "description": "E.164" } },
    "required": ["phone"]
  },
  "annotations": { "readOnlyHint": true }
}

A minimal server

With the official TypeScript SDK, one tool, on Express. Run it on a public HTTPS host and enter its URL in the dashboard.

server.ts
import express from "express";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

const TOKEN = process.env.SELER_MCP_TOKEN;

function build() {
  const server = new McpServer({ name: "erp-bridge", version: "1.0.0" });

  server.registerTool(
    "get_balance",
    {
      description: "The customer's account balance and loyalty points, by phone number.",
      inputSchema: { phone: z.string() },
      annotations: { readOnlyHint: true },
    },
    async ({ phone }) => {
      const account = await erp.accountByPhone(phone);
      if (!account) return { content: [{ type: "text", text: "No account for this number." }] };
      return {
        content: [{ type: "text", text: `Balance ${account.balance} IQD, ${account.points} points.` }],
        structuredContent: { balance: account.balance, points: account.points },
      };
    },
  );

  server.registerTool(
    "create_ticket",
    {
      description: "Open a support ticket for the customer. Changes data.",
      inputSchema: { phone: z.string(), subject: z.string(), details: z.string() },
    },
    async (input) => {
      const ticket = await erp.openTicket(input);
      return { content: [{ type: "text", text: `Ticket ${ticket.number} opened.` }] };
    },
  );

  return server;
}

const app = express();
app.use(express.json());

app.post("/mcp", async (req, res) => {
  if (req.get("authorization") !== `Bearer ${TOKEN}`) return res.status(401).end();
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
  const server = build();
  res.on("close", () => { transport.close(); server.close(); });
  await server.connect(transport);
  await transport.handleRequest(req, res, req.body);
});

app.listen(3333);

create_ticket has no readOnlyHint, so the owner sees it as “يغيّر بيانات” and, by default, the assistant asks the customer before calling it.

Limits

Servers per workspace5
Tools kept per server40The first forty listed.
Tool name≤ 64 charsLetters, digits and underscore survive; anything else becomes _.
Description≤ 1,000 charsShown to the owner and to the model.
Answer size256 KBLarger answers are dropped.
Text shown to the model8,000 charsThe text parts, joined.
Timeouts8 s call · 10 s list

Checklist

  • Streamable HTTP at one https:// URL on a public host.
  • The Authorization header checked on every request.
  • Every read-only tool carries annotations.readOnlyHint: true.
  • Descriptions say what the tool does in one sentence a shop owner can read.
  • Errors come back as isError: true with a sentence, not a stack trace.
  • A tool that reveals personal data trusts nothing about who is asking.