# Custom MCP servers

> An MCP server from an archive, a GitHub repository or by address: what will run, your consent, and only read-only tools — as you mark them.

Besides integrations from the [catalogue](/en/docs/connector/marketplace), the connector can run
**an MCP server you brought yourself**: your vendor’s server, a server from an open
repository, a server your team wrote for an internal system. The model gets its tools
the same way as the tools of catalogue integrations — under the same rules and along
the same path.

There is one difference: **nobody has checked** this code. Catalogue integrations are signed by us,
go to the network only through the connector’s core and only to the addresses you specified. Your own
server runs as is — under a separate OS user, but it goes to the network by itself, and
the connector neither sees nor limits its requests. So you confirm such a connection
explicitly, it is marked as **custom** everywhere, and which of its tools
count as read-only is decided by you, not by the server itself.

_Illustration: In the app such a connection is marked, and a tooltip explains what that means. Tools — only those the administrator marked as read-only._

## Three ways to bring a server

All three are on the [connector’s local page](/en/docs/connector/web-setup), in the
**“Your own MCP”** section. The code ends up on the connector’s machine and nowhere else; the installation is
confirmed by whoever sits at this page.

_Illustration: The “Your own MCP” section: drop an archive, name a repository or an address. At the top the page says what is on the machine — Node.js, Python._

**Archive.** Drop a file onto the page — the connector unpacks it and reads how this
server is started from its own packaging:

| Format | What is read |
|---|---|
| `.mcpb`, `.dxt` — MCP Bundle | `manifest.json`: the command, arguments, environment variables, settings the server asks the user for (`user_config`), the list of tools. The dependencies are already in the bundle |
| `.zip`, `.tar.gz`, `.tgz` with Node.js source | `package.json`: the entry point (`bin`, otherwise `main`); dependencies are installed with `npm install` (or `npm ci` if there is a lockfile); if the entry point needs building — `npm run build` |
| an archive with Python source | `pyproject.toml`: the server is installed into its own `.venv` environment and started with its script from `[project.scripts]` or as a module |
| an archive with an MCP registry `server.json` | name, version, the environment variables the server requires; how to start it — from the `package.json`/`pyproject.toml` lying next to it, and without them — the registry package via `npx`/`uvx` |

The connector removes the top-level folder added by GitHub or an archiver itself.
Links, absolute paths and escaping the directory are not allowed in an archive — such an archive
is rejected as a whole rather than “cleaned up”.

**GitHub repository.** Paste the address — with a branch, tag or commit if needed
(`…/tree/release-2`). A private repository needs a token: it is used once for
the download and not saved. Then everything is as with an archive.

**Remote server.** A server that is already running somewhere at an address (the
Streamable HTTP transport). Give the address and, if the server requires it, an `Authorization` header —
it is kept in the connector’s secrets directory and sent with every request. No code reaches your
machine in this case. OAuth sign-in for remote servers is not supported.

## What the machine needs

A Node.js server needs Node.js, a Python server needs Python 3 (or `uv`). The connector
looks for them in `PATH` **the way the connection’s user will see them** — an interpreter in
root’s home directory is not available to it. The page says what was found before you
drop anything; if what is needed is missing, the draft says so right away, and nothing is
saved. For Python servers the runtime the connector keeps for
catalogue integrations will also do.

<Callout>
Dependencies are installed **as the connection’s user**, in its directory, not as
the connector: `npm install` of someone else’s project runs someone else’s scripts, and that is the first place
where foreign code gets control. The installation log — word for word what
`npm` or `pip` printed — stays in the draft.
</Callout>

## What will be run

The draft shows **everything that will run**: the command, arguments, environment variables,
the settings the server asks for. This is a starting point, not a verdict: any field can be
edited, and exactly what is written in the form will run.

_Illustration: The draft after unpacking: what will run, what the server asked for, the installation log — and the consent without which nothing is saved._

- The packaging’s words `node`, `npx`, `python`, `uvx` are replaced by the connector with what it found on
  this machine.
- `${__dirname}` is the directory with the server’s code, `${user_config.name}` is the value of a setting from
  the form. This is how MCP Bundle writes its manifests; you can write it by hand the same way.
- A setting the packaging marked as secret is stored as a secret — in a file with
  `0600` permissions, with only a reference left in the settings file — and reaches the server
  only where there is a `${user_config.…}` for it. A secret the packaging said nothing
  about (a token for an API that the server reads from the environment) is added in the
  “Secrets in the environment” field — it reaches the server as a variable under its own name.
- A missing setting is a start-up error naming it, not an empty
  string passed to the server.

The model cannot see a name in Cyrillic — the
[slug](/en/docs/connector/tools#the-connector-registers-them-not-you) goes into tool names, and for such a name it
is taken from the server’s name in its packaging (`weather-mcp` → `weather_mcp`).

## Consent

A connection can be saved only with the checkbox ticked: you accept that the code has not been checked by
Buff, that the connector does not limit its network requests, and that the model will get only
the tools you mark as read-only. This is not an interface formality: the consent
is written into the [settings file](/en/docs/connector/yaml#mcp--your-own-server) as the
`accepted` field, and without it the connector **will not start** such a connection — even if it was written
into the YAML by hand.

The server is isolated like any connection: [its own process under its own OS user](/en/docs/connector/isolation),
its own working directory; it sees neither the connector’s key nor other secrets. Without the privilege
to switch users (not root, Windows) your own servers, like catalogue integrations, do not
start.

## Read-only is your decision

Tools reach the model [only if they read](/en/docs/connector/tools#writing-tools-are-never-given-to-the-model) —
in “Ask” and in deep research, as with catalogue integrations.
For a catalogue integration, its signed manifest says which tool is read-only.
Your own server has no signature — only its own word (`readOnlyHint` in the tool’s
description), and the word of unchecked code about its own harmlessness is exactly what
the “reading allowed, writing not” rule must not believe.

_Illustration: Your own server’s tools are checkboxes. “Server: read” is its word; the mark is your decision. Without a mark the model does not get the tool._

That is why your own server’s tools are checkboxes. As soon as the server has started and declared
them, tick those that only read and click **“Save read marks”**. The
**“Mark as the server suggests”** button ticks the boxes according to its hint — check them
with your own eyes. The marks apply immediately, without restarting the server; **an unmarked tool
is not given to the model at any stage**.

## Connectivity check and log

**“Check connection”** for your own server is a successful handshake: the name and version the
server introduced itself with, and the number of tools. **“Log”** is what the server writes to
standard error; a server that failed at start-up leaves the reason there.

## What it does not do

- **Events, polling, webhooks** — none: your own server gives the model tools, and only them.
  Requests from a tracker are a job for [catalogue integrations](/en/docs/connector/trackers).
- **MCP resources and prompts** — are not read; only `tools` are used.
- **Updates** — are not tracked. A new version is brought in the same way as the first; the old
  connection can be deleted.
- **OAuth** for remote servers and the outdated HTTP+SSE transport — are not supported.
- **Limiting the network** of foreign code — is not promised. That is exactly the reason for the mark.
