MCP and connected tools

Connect Nessie to MCP servers, and pair an outside agent so it can work in Nessie as you.

What MCP is

The Model Context Protocol is a small, open convention for describing tools to a language model and calling them. A server declares what it can do — a list of named tools, each with a JSON schema for its arguments — and a client discovers that list and invokes them on the model’s behalf. It is deliberately dull: the value is that one integration written once works with every model and every product that speaks it.

Nessie sits on both sides of it

That is worth being precise about, because the two halves are easy to confuse and they live at deliberately different paths.

  • Outbound. Nessie is an MCP client. You connect servers somebody else runs, and the tools they expose become tools your agents can use. The management surface is /api/mcp/*.
  • Inbound. Nessie is also an MCP server. An agent running somewhere else — Claude Code or Codex on a developer’s machine — can call into your boards and documents. That endpoint is POST /mcp, outside /api on purpose, so the two opposite meanings are not one path segment apart.

Connecting an MCP server to Nessie

Catalog entries and instances

There are two objects, and the distinction matters:

  • A catalog entry is the definition of a server — its name, its transport, how it authenticates. It is written once and can be shared.
  • An instance is that entry installed at one scope, with the credentials that reach it. Installing the same entry for two different teams produces two instances.

Most people never touch either directly. The doorway in the admin is the app store at /admin/apps: it browses the same catalog with the connection details hidden, and Connect on a listing creates an instance. Operators who want the raw objects use /api/mcp/catalog and /api/mcp/instances.

If a server is not in the catalog yet, POST /api/mcp/discover takes a URL and reads back what the server says about itself, and POST /api/mcp/library/import pulls an entry from the public server library. A first-run connection can also be created straight from a URL with POST /api/apps/custom.

Transports

Nessie connects to remote servers over HTTP. Two transports work: streamable HTTP (the current MCP transport) and the older HTTP+SSE transport. Pick http unless the server only offers sse.

The stdio transport is refused for connectors you author. stdio means the MCP server is a local process the host starts, and running arbitrary programs inside the Nessie server is not something an operator can meaningfully review. Servers that are published only as a package to run locally are dropped from library imports for the same reason. A ws value exists in the protocol field and is not implemented — a connection using it fails when Nessie tries to open it.

How Nessie authenticates to the server

A catalog entry declares one of five methods: none, api_key, bearer, basic or oauth2.

api_key is header injection, and the entry says which header and what prefix — so Authorization: Bearer …, X-API-Key: … and X-Auth-Token: Token … are all the same method configured differently. The key itself is pasted once with POST /api/mcp/instances/:instanceId/secret and is never readable again; no response from the instance routes carries a credential reference.

oauth2 comes in two shapes. A server with fixed endpoints has them in its catalog entry. A server that publishes standard metadata needs nothing configured at all: Nessie reads the protected-resource and authorization-server documents, and mints itself a client through dynamic client registration or through the client-ID metadata document it publishes at its own well-known path. Either way the handshake is POST /api/mcp/instances/:instanceId/oauth/start followed by the provider’s redirect back to GET /api/mcp/oauth/callback. The state token is single-use, held in the database and valid for ten minutes, and PKCE is always used.

One rule surprises people: even an organization owner cannot attach a personal or OAuth token to somebody else. A shared credential is only possible for an API-key entry that has been validated as one; anything else is refused. A credential that belongs to a person stays a credential that belongs to that person.

Where the credentials live

Connector secrets and OAuth token bundles are stored in your own Postgres database, in purpose-bound AES-256-GCM envelopes under the deployment’s independently versioned encryption key ring, and addressed only by opaque secret_* references. They are deliberately not in the external secret vault that holds other Nessie secrets. A production deployment that has not configured the encrypted store refuses to start rather than complete an OAuth handshake it would then drop on the floor.

Where a connection reaches

An instance is installed at one of six scopes: system, organization, project, team, channel or user. That scope is a hard ceiling on which runs can use it, and grants can only narrow it further, never widen it — so a connector you installed with your own account cannot quietly become an organization-wide one.

Who may install where follows the same shape: organization owners and admins manage installs at any scope; everyone else installs connectors at their own user scope, and can see shared installs they are entitled to reach.

Giving an agent a tool

Connecting a server does not hand its tools to anybody. When an instance is installed, Nessie probes it and projects each tool it advertises into the tool registry, which is what the admin shows at /admin/advanced/tools — built-in tools, MCP tools, bundle tools and executor tools in one list, each with its source and status.

A projected tool has one of three statuses: active, pending_review or disabled. Tools from a shared-scope install arrive as pending_review and have to be approved on that page before an agent sees them; tools from your own user-scope install are active straight away, because the only person exposed is you. Review is recurring, not a one-off ceremony: if a later probe finds that a tool’s description or schema has drifted, it goes back to pending_review and stops being offered until somebody looks at the change.

Which agents may then call an active tool is decided one of two ways:

  • A connection created through the app store requires an explicit grant. An agent gets the tool only when a grant names it, and the grant is checked against the tool’s current fingerprint, so a changed tool needs a fresh decision.
  • Otherwise the tool is on by default for the agents in reach of its scope, and individual agents are switched off on the tool’s detail panel.

A grant names exactly one principal — a role or an agent, never both — and a deny beats an allow. For a user-scope connection the question does not arise: any agent that person talks to can use it, and an explicit switch-off is the only thing that withholds it.

One thing that is easy to look for in the wrong place: /settings/accounts in the admin is connected accounts — Slack, mailboxes, model subscriptions and the OAuth links to Linear, Jira, GitHub and Trello that back external boards. MCP connectors are at /admin/apps, and the tools they produce are at /admin/advanced/tools.

Approval gates on tool calls

Some tool calls stop and wait for a person. When one does, the run does not fail: the invocation is frozen — the tool name, a hash of its arguments and enough state to resume — and an approval request appears in /api/approvals for somebody to answer. Resuming is idempotent, so a duplicated approval cannot run the call twice. The person deciding sees a bounded summary of what is being asked; the exact arguments and the originating run stay server-side.

There are two distinct gates, and they are configured differently.

  • Structural gates are a property of certain built-in tools and are not configurable. Sending mail from an agent’s own mailbox or from a connected one, sending a Gmail draft, disconnecting a mail account, creating, changing or canceling a calendar event, and a browser action that would write to a site the browser is not signed in to are all gated this way. They get a 24-hour window to be answered rather than the usual 30 minutes.
  • Policy gates come from policy rules on the tool/invoke pair. A rule that allows an invocation can carry requiresApproval, and without a verified approval the evaluation returns APPROVAL_REQUIRED rather than running. Rules are evaluated deny-first and ordered by scope.

Which admin screen authors a requiresApproval policy rule for a tool — the data model and the enforcement are both in place, but we could not confirm the screen that writes such a rule.


Connecting an outside agent to Nessie

The other direction. Nessie runs an MCP server of its own at POST /mcp, over streamable HTTP, and it is stateless: every call carries its own credential and resolves its own actor, so there is no session to pin a client to one replica. ChatGPT, Claude, Claude Code, Codex or an agent on somebody’s laptop can work on your boards, documents and customers from inside whatever tool that person already uses. The step-by-step for each app is Connect ChatGPT, Claude and other AI apps.

What a paired credential actually is

This is the part to get right, because the obvious mental model — an API key with permissions attached — is the wrong one.

A paired credential is never an identity. It names a human, and every tool call runs as that human, against the same service functions the HTTP routes call. An agent therefore cannot reach anything the person who lent it could not reach by clicking. Scopes only ever narrow that reach; there is no scope that widens it.

Liveness is re-read on every single call, never trusted from the token: revocation, expiry, the user’s token version, their organization membership and their current role. A demotion, a deactivation or a “sign me out everywhere” lands on the agent’s next call rather than at the credential’s expiry.

The token is opaque, prefixed nag1_, stored only as a SHA-256 hash, and lives 90 days. It is refused with a specific 403 on any route other than the MCP endpoint — presenting it elsewhere is not a partial success to retry, which is what keeps “I lent an agent my boards” from meaning “I lent it my account”.

Two ways to connect

An app that can open a browser — ChatGPT, Claude, Claude Code, Codex — uses the MCP authorization profile: OAuth 2.1 with PKCE (S256), dynamic client registration at POST /mcp/oauth/register, refresh tokens and resource indicators. The app sends the person to /settings/security, they choose what it may do and which of their teams it reaches, and the browser goes back to the app with a code. Its access token lasts an hour and is refreshed; the connection lives 90 days.

A command-line agent controls no browser and can host no callback URL, so it pairs with the device authorization grant (RFC 8628) instead.

  1. The agent calls POST /mcp/auth/device with a clientName and the scopes it wants. It gets back a device_code, a short user_code, a verification_uri and a verification_uri_complete with the code already in the query, plus the poll interval and an expires_in. It prints the URI.
  2. A person opens that URI while signed in, at /settings/security. They see which agent is asking, whose account it would work as, until when, and exactly what it would be able to do — and they choose Allow or Don’t allow.
  3. The agent polls POST /mcp/auth/token with the deviceCode, honoring authorization_pending, slow_down, expired_token and access_denied. On success it receives { access_token, token_type, scope, expires_at }.

Either way there is a human step, because the credential inherits a person’s entitlements, so a person has to choose to lend them. Nothing here mints access from nothing. Connecting can also be switched off for a whole organization at /admin/security?tab=programs, and that answer is checked when somebody allows a connection, not when an agent starts one.

Discovery

An unauthenticated POST /mcp answers 401 with a WWW-Authenticate header pointing at /.well-known/oauth-protected-resource (RFC 9728), which names this origin as the authorization server; its metadata at /.well-known/oauth-authorization-server (RFC 8414) names the registration, authorization, token and revocation endpoints. That chain is everything a client needs once it has the address. To find the address from a domain, an agent reads /.well-known/ai-catalog.json, which names the Server Card at /mcp/server-card (MCP Server Cards, SEP-2127). The device grant’s endpoints are listed in the protected-resource document under x-nessie-device-authorization.

A deployment can also answer at a second, shorter address: Nessie Cloud’s https://nessie.works/mcp is proxied to the API, and a client that came in that way is given that address back as the resource, while signing in stays at the API. A redirect would not do, because a client refuses metadata naming any address but the one it was given. The operator lists these origins in NESSIE_MCP_ALIAS_ORIGINS.

Scopes and tools

Scopes are coarse on purpose — a scope nobody can explain is a scope nobody sets correctly. A connection that reaches more than one team lists them with nessie_team_list, and every other tool then names its teamId.

ScopeReaches
boards_readProjects, boards and tickets, with their comments, files, labels, types and checklists
boards_writeCreating, editing and moving tickets, and everything on them
documents_readKnowledge spaces, documents and spreadsheets
documents_writeDrafting and editing them, and asking to publish one
clients_readThe customer inbox, contacts, contact lists, newsletters and contact forms
clients_writeNotes, taking, assigning and closing conversations, contact lists and preparing newsletters
clients_sendAnything a customer receives: a reply, a first email or a text. Never checked unless the person checks it
website_toolsThe page tools of your team’s registered website, operated in a hosted browser you may control. Never ticked unless the person ticks it

A call without the scope it needs comes back as a tool result naming the scope to ask for, not as a transport error, so the model can report something useful instead of retrying into a wall.

There is no separate family of tools for external trackers. A board is a board whether its tasks originate in Nessie or are mirrored from Linear, Jira, GitHub or Trello, so one tool set covers both, and every task says which it is. What differs is writing: a board mirrored from a read-only source refuses a write in words, telling the agent that the other system owns the ticket and that retrying will not help.

Publishing a document is an approval, never a scope

Agents draft; a person publishes. That rule is enforced for Nessie’s own agents by refusing an agent actor outright — but a paired credential resolves as a person, so that refusal cannot catch it. The gate has to be somewhere else.

So nessie_doc_publish does not publish. It opens a knowledge.page.publish approval — the same one a Nessie agent opens — and returns { status: "awaiting_approval", approvalId }. Polling returns the same request rather than opening a second one. The decision is made the way every other publication decision in Nessie is made: by a person, about that document, at the moment it exists.

There is deliberately no publish scope. There was one — a checkbox at pairing time saying this agent may publish — and it was retired. It decided, once and for ninety days, a question the product asks per document everywhere else, and it decided it before the document existed, so the person checking it could not know what they were agreeing to publish. nessie_doc_publish needs documents_write, because asking is not free — it reads the page and puts a decision in front of somebody — but that scope grants nothing about publication.

Two identities are recorded on the request, and the distinction is load-bearing. The requester is the credential, never the human it acts as. The required approver is that human, and only they may answer. If the request named the person as its requester, the rule that stops anybody approving their own request would disqualify the one person entitled to decide.

Seeing and revoking what is paired

A person’s own paired agents are listed at /settings/security, with a label and a revoke control for each. An owner or organization admin sees every credential in the organization at /admin/security?tab=programs — what each is called, what it may do, whether it is live, and whose account it borrows — and can revoke any of them. The personal list stays self-only: a list of live credentials is a list of footholds, not general reading.

Writes made through a credential are attributed to the borrowing human and carry the credential’s id in the audit metadata, so the log can tell a person’s own edit from one their agent made for them.

What is not available yet

Being explicit, because the gap between a protocol’s capabilities and a given server’s is where documentation usually lies:

  • Chat, channels, runs and agent management are not exposed over MCP.
  • A program prepares a newsletter and a person sends it: nothing sends, schedules or deletes a campaign, and the email itself is written in the designer.
  • Linking and merging contacts, CRM schema changes, imports and channel setup stay with a person in the browser.
  • The connecting switch is an organization-level setting. Team-level and personal overrides are not something a team or a person can set today.