Connect Your MCP Server to Ajeris
Ajeris speaks the Model Context Protocol. If you operate an MCP server - yours, your company's, or one from the community - you can point Ajeris at it and the agent will reach every tool the server exposes alongside Ajeris's built-in integrations on the next message you send.
This page is for the engineer or operator setting up the server. It covers what protocol Ajeris speaks, what it sends to your server, how to connect, and what to write in a tool's description so the agent will actually call it.
Prerequisites
To plug in a server you need:
- A Streamable HTTP MCP server on a reachable HTTPS URL. Plain
http://,localhost, private-network, and link-local addresses are rejected at validation time. - An auth credential (optional). Whatever your server expects
in the
Authorizationheader - typicallyBearer <token>or a raw API key. Ajeris encrypts this at rest before storing it and sends it verbatim on every request. - At least one tool registered. Servers that return an empty tool list are rejected at preview time.
The server must implement MCP Streamable HTTP (the current transport as of the MCP 2025-06-18 spec). Stdio servers cannot be reached from Ajeris's cloud-hosted agent service.
Connect via the web UI
- Text
manageto your Ajeris number. The agent replies with a personal management URL. The link is single-use and expires on a short TTL. - Open the URL. Under Your own, tap Connect your own tools.
- Fill the form:
- Label - any short name you'll recognize. Max 80 chars.
- URL - the HTTPS endpoint, e.g.
https://mcp.example.com/mcp. Non-HTTPS URLs are rejected. - Authorization header - paste the entire header value
verbatim, including the scheme if any (
Bearer sk-live-…,Token abc123, or justabc123if your server doesn't care about schemes). Leave blank for unauthenticated servers.
- Tap Check connection. Ajeris opens a real MCP session
against your URL - it doesn't just probe the TCP port. If the
round-trip succeeds, you'll see the full list of tools the
agent will see, verbatim from your server's
listTools()response. - Review the tools. Every tool name + description comes straight from your server. If anything is wrong (unexpected tool, typo in an arg schema, stale build), fix on the server side and re-validate.
- Tap Connect. Your next message to the agent sees the new tools.
The page also lists already-connected servers with Disable and Remove buttons. Disabling skips the server at agent-turn load time without deleting it.
What Ajeris sends to your server
Ajeris calls your server twice in two different moments:
- At connect time, when you tap Check connection: a single
Streamable HTTP session, an MCP
initialize, thenlistTools, then close. A 10-second timeout applies. - On every agent turn after that, while the server is enabled: a fresh Streamable HTTP session is opened, the tool list is fetched, and any tool calls the model decides to make are routed through that session. The session closes at the end of the turn.
Both flows send only one header from your config: the
Authorization value you pasted, sent as-is. The MCP SDK adds the
standard Streamable HTTP protocol headers (Accept,
Content-Type, Mcp-Session-Id on follow-up requests, etc.) -
those are protocol concerns, not Ajeris identity.
Ajeris does not currently forward user identity, tenant ID, or conversation metadata to your server. If you need to know which Ajeris user is calling your tools, either:
- Issue one long-lived token per user and infer identity from the token, or
- Host a proxy in front of your MCP server that maps a shared token to per-user identity using some out-of-band signal (IP allowlist, header signed by the proxy, …).
No CORS required. Ajeris connects server-to-server, never from
a browser. Your MCP server does not need to set
Access-Control-Allow-Origin or any other CORS header. If a code
generator or scaffolding tool added CORS middleware to your MCP
route, you can remove it.
Security
- HTTPS only. Non-HTTPS URLs are rejected at validation.
- SSRF guard. Loopback (
127.0.0.0/8,::1,localhost), RFC1918 (10/8,172.16/12,192.168/16), link-local (169.254/16,fe80::/10), and ULA (fc00::/7) hostnames are rejected. Deploy on a public host. - Encryption at rest. Your
Authorizationheader is encrypted before it touches storage and decrypted only inside the agent service, for the turn that needs it. - Tenant isolation. A connected server is visible only to the user who connected it. Server connections cannot leak across Ajeris accounts.
- Single header per server. Ajeris sends only the
Authorizationvalue you configured. It does not add cookies, forwarded identity, user metadata, or tracing headers. If your server requires additional auth (X-Company-Tenant, mTLS, …), host a thin proxy that injects them after validating the AjerisAuthorization. - Connection lifetime. A fresh client is opened per agent turn and closed at the end. There is no long-lived connection to your server between turns.
Errors you might see
Every validation and save response carries a structured reason
that maps to a specific UI message and a specific remedy. The
reasons you'll actually see while integrating:
reason | When it happens | Fix |
|---|---|---|
invalid_url | URL fails to parse | Check for typos; URL must include protocol + host |
http_not_allowed | URL is http:// or any non-HTTPS scheme | Terminate TLS in front of your server (Cloudflare, Railway, AWS, etc.) |
blocked_host | Hostname matches the SSRF block-list | Deploy your server to a public host, not a private network or your laptop |
unreachable | TCP/HTTP connect failed or timed out (>10s) | Check DNS, firewall, server is running, and responds within 10 seconds |
auth_failed | Your server returned 401 or 403 during connect | Rotate/verify the token; check scheme (Bearer vs Token vs bare) |
protocol_error | Server connected but didn't speak MCP | Verify you're serving the Streamable HTTP transport, not SSE-only or custom JSON-RPC |
no_tools | listTools() returned an empty array | Register at least one tool before saving |
invalid_token | Management link expired between page-load and submit | Text manage again for a fresh link |
Build a minimum working MCP server
This section is the canonical recipe. The pattern below is the
shape Ajeris's validator expects and has been verified end-to-end
(Bearer-auth gate + listTools() round-trip).
TypeScript
npm install @modelcontextprotocol/sdk@^1.0.0 express zod// server.ts
import express from 'express';
import { randomUUID } from 'node:crypto';
import { z } from 'zod';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
const SECRET = process.env.MCP_BEARER!; // set in your host's env
const app = express();
app.use(express.json());
// Bearer-auth gate. The user pastes "Bearer <SECRET>" into the
// Authorization header field on the Ajeris connect form.
app.use('/mcp', (req, res, next) => {
if (req.header('authorization') !== `Bearer ${SECRET}`) {
res.status(401).json({ error: 'unauthorized' });
return;
}
next();
});
const transports = new Map<string, StreamableHTTPServerTransport>();
function buildServer(): McpServer {
const mcp = new McpServer({ name: 'my-mcp-server', version: '1.0.0' });
// One illustrative tool. Replace with whatever your service does;
// register one tool per discrete operation you want the agent
// to be able to perform.
mcp.registerTool(
'example_echo',
{
description: 'Echoes the input string back. Replace with a real tool.',
inputSchema: { text: z.string() },
},
async ({ text }) => ({
content: [{ type: 'text', text: `you said: ${text}` }],
}),
);
return mcp;
}
// Single endpoint, three verbs - this is the Streamable HTTP shape.
const handle: express.RequestHandler = async (req, res) => {
const sid = req.header('mcp-session-id');
let t = sid ? transports.get(sid) : undefined;
if (!t) {
if (!isInitializeRequest(req.body)) { res.status(400).end(); return; }
t = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
});
const mcp = buildServer();
await mcp.connect(t);
t.onclose = () => { if (t!.sessionId) transports.delete(t!.sessionId); };
await t.handleRequest(req, res, req.body);
if (t.sessionId) transports.set(t.sessionId, t);
return;
}
await t.handleRequest(req, res, req.body);
};
app.post('/mcp', handle);
app.get('/mcp', async (req, res) => {
const t = transports.get(req.header('mcp-session-id') ?? '');
if (!t) { res.status(404).end(); return; }
await t.handleRequest(req, res);
});
app.delete('/mcp', async (req, res) => {
const t = transports.get(req.header('mcp-session-id') ?? '');
if (!t) { res.status(404).end(); return; }
await t.handleRequest(req, res);
});
app.listen(process.env.PORT ?? 8080);That is a complete server. Deploy it behind HTTPS (any host that
terminates TLS - Railway, Fly.io, Cloudflare Workers, AWS App
Runner, Vercel Functions, Kubernetes, bare VM) and hand the user
the public URL plus the Bearer <SECRET> value to paste into
Ajeris.
Notes on the pattern:
- Build a fresh
McpServerper session. A givenMcpServerinstance can only be connected to one transport at a time; reusing one across sessions breaks the second connection. - Keep the
transportsmap keyed bymcp-session-idand clean up ononcloseso abandoned sessions don't leak. - The first POST without a session ID must be the JSON-RPC
initializerequest - that's whatisInitializeRequestchecks. - The route handles three verbs on one path: POST for requests,
GET for server-streamed responses, DELETE for session cleanup.
Don't expose two paths (
/sse+/message); that's the legacy HTTP+SSE transport, deprecated as of MCP 2025-03-26.
Python
pip install mcp uvicornThe Python SDK ships an equivalent streamable_http transport.
See github.com/modelcontextprotocol/python-sdk
for the canonical example.
Tool naming
- Match
^[a-zA-Z0-9_-]+$. - Keep names ≤ 64 characters.
- Prefix with a short namespace that identifies your server so tools are recognizable in the agent's tool list and don't collide with built-in tools.
Writing tool descriptions (this is the most important part)
Ajeris is SMS-first. The agent picks tools by reading natural-language messages from the user and matching intent to a tool description. Bad descriptions silently break the integration: the agent sees the tool, doesn't recognize when to use it, and replies "I don't have a way to do that."
Write each description so that it:
- Starts with the user-facing job the tool does, in plain English.
- Mentions the kinds of phrasings a user might use to ask for it.
- Says what the tool returns or changes.
A weak description ("Returns rows", "Calls the API") is the most common reason a freshly-connected MCP server appears in the preview but never gets called by the agent. The shape of the input schema is not enough on its own; the model leans heavily on the prose.
Argument descriptions matter too. The agent fills arguments from
the message context, so a description like "ISO 8601 date, e.g. 2026-04-28" produces far better calls than just string.
When the user pastes your URL into Ajeris and clicks Check connection, the preview screen shows your tool list with these descriptions verbatim. That preview is the user's last chance to catch a missing tool or unclear copy before saving.
For more reference servers, see github.com/modelcontextprotocol/servers.
Finding community servers
If you don't want to build your own:
- Docker MCP Catalog - 300+ verified servers, install from
Docker Desktop or
docker mcpCLI. - MCP Registry - registry.modelcontextprotocol.io, the official cross-company catalog.
- PulseMCP - pulsemcp.com/servers, thousands of indexed community servers.
- Smithery - smithery.ai, with automated install guides.
Not every community server ships an HTTPS deployment. If a server is stdio-only, you'll need to host it yourself behind HTTPS before you can connect it through the web UI.
Troubleshooting
"Could not reach the server." (unreachable)
- Is the host publicly resolvable?
dig +short your-host.example.comfrom an off-network shell. - Does it respond inside 10s?
curl -v -m 10 https://host/mcp. - Is the server bound to
0.0.0.0(not127.0.0.1)? Behind a firewall, a127.0.0.1binding looks reachable but fails from outside.
"Server rejected the auth header." (auth_failed)
- Paste the exact header VALUE, including the scheme your server
expects.
Bearer abcvs bareabcvsToken abcare all different. - Rotate the token on the source service and try again. Some tokens silently expire on first use or after a TTL.
"Server connected but exposes zero tools." (no_tools)
- Your server is up but
listTools()returned{ tools: [] }. Check the server's registered tool set.
"Protocol error." (protocol_error)
- Almost always means the endpoint isn't speaking Streamable HTTP. SSE-only servers and custom JSON-RPC endpoints won't work.
- Double-check your route mounts
StreamableHTTPServerTransport.
"Too many tools slow down responses."
- Each connected server's tools consume context on every turn. A server with dozens of tools meaningfully grows the per-turn prompt size.
- Disable the ones you don't use daily from the same page. Per-tool filtering is on the roadmap.
Roadmap
- Multiple auth headers - beyond the single
Authorizationfield. - OAuth catalog - "Connect GitHub", "Connect Linear" buttons that pre-fill the URL and walk through OAuth.
- Per-tool filtering - choose which subset of a server's tools to expose to the agent, cutting token overhead on multi-tool servers.
- Marketplace - browse + one-click install community servers during onboarding.