nessie

MCP en verbonden tools

Verbind Nessie met MCP-servers en koppel een externe agent, zodat die namens jou in Nessie kan werken.

Wat MCP is

Het Model Context Protocol is een kleine, open conventie om tools te beschrijven aan een taalmodel en ze aan te roepen. Een server geeft aan wat hij kan: een lijst met benoemde tools, elk met een JSON-schema voor de argumenten. Een client ontdekt die lijst en roept de tools namens het model aan. Het is bewust saai: de waarde zit erin dat één integratie, één keer geschreven, werkt met elk model en elk product dat het spreekt.

Nessie zit aan beide kanten

Dat is de moeite waard om precies te zijn, want de twee helften worden makkelijk verward, en ze lopen bewust over verschillende paden.

  • Uitgaand. Nessie is een MCP-client. Je koppelt servers die iemand anders beheert, en de tools die ze aanbieden worden tools die je agents kunnen gebruiken. Het beheeroppervlak is /api/mcp/*.
  • Inkomend. Nessie is ook een MCP-server. Een agent die ergens anders draait — Claude Code of Codex op de computer van een ontwikkelaar — kan je borden en documenten aanspreken. Dat endpoint is POST /mcp, bewust buiten /api, zodat de twee tegengestelde betekenissen niet naast elkaar in één padsegment staan.

Een MCP-server koppelen aan Nessie

Catalogusitems en instanties

Er zijn twee objecten, en het onderscheid doet ertoe:

  • Een catalogusitem is de definitie van een server: de naam, het transport en hoe hij authenticeert. Het wordt één keer geschreven en kan worden gedeeld.
  • Een instantie is dat item geïnstalleerd op één niveau, met de inloggegevens die erbij horen. Hetzelfde item voor twee verschillende teams installeren levert twee instanties op.

De meeste mensen raken geen van beide direct aan. De ingang in de admin is de app store op /admin/apps: die bladert door dezelfde catalogus zonder de verbindingsdetails te tonen, en Verbinden op een vermelding maakt een instantie aan. Beheerders die de ruwe objecten willen gebruiken, gebruiken /api/mcp/catalog en /api/mcp/instances.

Staat een server nog niet in de catalogus, dan neemt POST /api/mcp/discover een URL en geeft terug wat de server over zichzelf zegt. POST /api/mcp/library/import haalt een item uit de publieke serverbibliotheek. Een eerste verbinding kan ook meteen vanuit een URL worden gemaakt met POST /api/apps/custom.

Transporten

Nessie verbindt met externe servers via HTTP. Er werken twee transporten: streamable HTTP (het huidige MCP-transport) en het oudere HTTP+SSE-transport. Kies http, tenzij de server alleen sse aanbiedt.

Het stdio-transport wordt geweigerd voor connectors die je zelf schrijft. stdio betekent dat de MCP-server een lokaal proces is dat de host start, en het is niet zinvol voor een beheerder om willekeurige programma’s binnen de Nessie-server te controleren. Servers die alleen als lokaal te draaien pakket worden gepubliceerd, vallen om dezelfde reden uit bibliotheekimports. Een ws-waarde bestaat in het protocolveld en is niet geïmplementeerd: een verbinding die die gebruikt, faalt zodra Nessie probeert hem te openen.

Hoe Nessie zich bij de server authenticeert

Een catalogusitem declareert één van vijf methoden: none, api_key, bearer, basic of oauth2.

api_key is header-injectie, en het item zegt welke header en welk voorvoegsel. Authorization: Bearer …, X-API-Key: … en X-Auth-Token: Token … zijn dus allemaal dezelfde methode, anders ingesteld. De sleutel zelf wordt één keer geplakt met POST /api/mcp/instances/:instanceId/secret en is daarna nooit meer leesbaar. Geen enkel antwoord van de instantieroutes bevat een verwijzing naar een credential.

oauth2 komt in twee vormen. Een server met vaste endpoints heeft die in zijn catalogusitem. Een server die standaardmetadata publiceert, vraagt helemaal niets om in te stellen: Nessie leest de documenten van de beschermde bron en de autorisatieserver, en maakt zelf een client aan via dynamische clientregistratie of via het client-ID-metadatadocument dat hij publiceert op zijn eigen well-known-pad. In beide gevallen is de handshake POST /api/mcp/instances/:instanceId/oauth/start, gevolgd door de redirect van de provider terug naar GET /api/mcp/oauth/callback. Het statustoken is eenmalig bruikbaar, staat in de database en is tien minuten geldig, en PKCE wordt altijd gebruikt.

Eén regel verrast mensen: zelfs een eigenaar van een organisatie kan geen persoonlijk of OAuth-token aan iemand anders koppelen. Een gedeelde credential is alleen mogelijk voor een API-sleutelitem dat als zodanig is gevalideerd; al het andere wordt geweigerd. Een credential die bij een persoon hoort, blijft een credential die bij die persoon hoort.

Waar de credentials staan

Connectorgeheimen en OAuth-tokenbundels worden opgeslagen in je eigen Postgres-database, in doelgebonden AES-256-GCM-enveloppen onder de onafhankelijk versiebeheerde versleutelingssleutelring van de deployment. Ze zijn alleen bereikbaar via ondoorzichtige secret_*-verwijzingen. Bewust staan ze niet in de externe secret vault die andere Nessie-geheimen bewaart. Een productiedeployment waarvoor de versleutelde opslag niet is ingesteld, weigert te starten, in plaats van een OAuth-handshake af te ronden die het daarna meteen zou laten vallen.

Waar een verbinding reikt

Een instantie wordt geïnstalleerd op één van zes niveaus: system, organization, project, team, channel of user. Dat niveau is een harde bovengrens voor welke runs het kunnen gebruiken, en grants kunnen het alleen verder beperken, nooit verruimen. Een connector die je met je eigen account hebt geïnstalleerd, kan dus niet stilletjes een connector voor de hele organisatie worden.

Wie waar mag installeren volgt dezelfde vorm: eigenaren en beheerders van de organisatie beheren installaties op elk niveau; iedereen anders installeert connectors op het eigen user-niveau, en kan gedeelde installaties zien waar hij toegang toe heeft.

Een agent een tool geven

Een server koppelen geeft zijn tools aan niemand. Wanneer een instantie wordt geïnstalleerd, controleert Nessie de server en projecteert hij elke tool die die adverteert in het toolregister. Dat register toont de admin op /admin/advanced/tools: ingebouwde tools, MCP-tools, bundeltools en executortools in één lijst, elk met zijn bron en status.

Een geprojecteerde tool heeft één van drie statussen: active, pending_review of disabled. Tools van een installatie op gedeeld niveau komen binnen als pending_review en moeten op die pagina worden goedgekeurd voordat een agent ze ziet. Tools van je eigen installatie op user-niveau zijn meteen actief, omdat de enige persoon die erbij komt jijzelf bent. Beoordeling is terugkerend en geen eenmalige ceremonie: als een latere controle vindt dat de beschrijving of het schema van een tool is veranderd, gaat die terug naar pending_review en wordt die niet meer aangeboden totdat iemand de wijziging heeft bekeken.

Welke agents een actieve tool mogen aanroepen, wordt op één van twee manieren bepaald:

  • Een verbinding die via de app store is gemaakt, vereist een expliciete grant. Een agent krijgt de tool alleen wanneer een grant hem noemt, en die grant wordt gecontroleerd tegen de huidige fingerprint van de tool. Een gewijzigde tool heeft dus een nieuw besluit nodig.
  • Anders staat de tool standaard aan voor de agents binnen bereik van zijn niveau, en individuele agents worden uitgeschakeld in het detailpaneel van de tool.

Een grant noemt precies één principal — een rol of een agent, nooit beide — en een weigering wint van een toestemming. Voor een verbinding op user-niveau speelt de vraag niet: elke agent waarmee die persoon praat, kan hem gebruiken, en alleen een expliciete uitschakeling houdt hem tegen.

Eén ding dat je makkelijk op de verkeerde plek zoekt: /settings/accounts in de admin gaat over gekoppelde accounts — Slack, mailboxen, modelabonnementen en de OAuth-koppelingen naar Linear, Jira, GitHub en Trello die externe borden ondersteunen. MCP-connectors staan op /admin/apps, en de tools die ze opleveren staan op /admin/advanced/tools.

Goedkeuringspoorten op toolaanroepen

Sommige toolaanroepen stoppen en wachten op een persoon. Wanneer dat gebeurt, mislukt de run niet: de aanroep wordt bevroren — de toolnaam, een hash van de argumenten en genoeg status om te hervatten — en er verschijnt een goedkeuringsverzoek op /api/approvals om door iemand te worden beantwoord. Hervatten is idempotent, dus een dubbele goedkeuring kan de aanroep niet twee keer laten draaien. De persoon die beslist, ziet een begrensde samenvatting van wat er gevraagd wordt; de exacte argumenten en de oorspronkelijke run blijven server-side.

Er zijn twee afzonderlijke poorten, en ze worden anders ingesteld.

  • Structurele poorten zijn een eigenschap van bepaalde ingebouwde tools en zijn niet configureerbaar. Mail versturen vanuit de eigen mailbox van een agent of vanuit een gekoppelde mailbox, een Gmail-concept versturen, een mailaccount loskoppelen, een agenda-afspraak aanmaken, wijzigen of annuleren, en een browseractie die zou schrijven naar een site waarop de browser niet is ingelogd, zijn allemaal op deze manier afgeschermd. Ze krijgen een venster van 24 uur om beantwoord te worden, in plaats van de gebruikelijke 30 minuten.
  • Beleidspoorten komen uit beleidsregels op het tool/invoke-paar. Een regel die een aanroep toestaat, kan requiresApproval dragen, en zonder geverifieerde goedkeuring geeft de evaluatie APPROVAL_REQUIRED terug in plaats van te draaien. Regels worden deny-first geëvalueerd en gesorteerd op niveau.

Welk admin-scherm een requiresApproval-beleidsregel voor een tool aanmaakt, hebben we niet kunnen vaststellen. Het datamodel en de handhaving zijn allebei aanwezig, maar het scherm dat zo’n regel schrijft, konden we niet bevestigen.


Een externe agent aan Nessie koppelen

De andere kant. Nessie draait zelf een MCP-server op POST /mcp, via streamable HTTP. Die server is stateless: elke aanroep draagt zijn eigen credential en bepaalt zijn eigen actor, dus er is geen sessie waaraan een client aan één replica vastzit. ChatGPT, Claude, Claude Code, Codex of een agent op iemands laptop kan met je borden, documenten en klanten werken vanuit de tool die die persoon al gebruikt. De stap-voor-stapuitleg voor elke app staat bij ChatGPT, Claude en andere AI-apps verbinden.

Wat een gekoppelde credential precies is

Dit is het deel dat je goed moet begrijpen, want het voor de hand liggende mentale model — een API-sleutel met rechten eraan — is het verkeerde.

Een gekoppelde credential is nooit een identiteit. Ze noemt een mens, en elke toolaanroep draait als die mens, tegen dezelfde servicefuncties die de HTTP-routes aanroepen. Een agent kan dus niets bereiken wat de persoon die hem leende niet zelf kon bereiken door te klikken. Scopes beperken dat bereik alleen maar; er is geen scope die het verruimt.

Geldigheid wordt bij elke aanroep opnieuw gelezen en nooit uit het token overgenomen: intrekking, vervaldatum, de tokenversie van de gebruiker, zijn lidmaatschap van de organisatie en zijn huidige rol. Een degradatie, een deactivering of een “log me overal uit” werkt door bij de volgende aanroep van de agent, en niet pas bij de vervaldatum van de credential.

Het token is ondoorzichtig, begint met nag1_, wordt alleen als SHA-256-hash opgeslagen en blijft 90 dagen geldig. Het wordt met een specifieke 403 geweigerd op elke route behalve het MCP-endpoint. Elders aanbieden is geen gedeeltelijk succes om opnieuw te proberen. Daardoor betekent “ik heb een agent mijn borden geleend” niet “ik heb hem mijn account geleend”.

Twee manieren om te verbinden

Een app die een browser kan openen — ChatGPT, Claude, Claude Code, Codex — gebruikt het MCP-autorisatieprofiel: OAuth 2.1 met PKCE (S256), dynamische clientregistratie op POST /mcp/oauth/register, refreshtokens en resource-indicators. De app stuurt de persoon naar /settings/security. Die kiest wat de app mag doen en tot welke van zijn teams de app reikt, en de browser stuurt de persoon terug naar de app met een code. Het toegangstoken geldt een uur en wordt ververst; de verbinding loopt 90 dagen.

Een agent op de opdrachtregel bestuurt geen browser en kan geen callback-URL hosten. Daarom koppelt hij met de device authorization grant (RFC 8628).

  1. De agent roept POST /mcp/auth/device aan met een clientName en de scopes die hij wil. Hij krijgt een device_code, een korte user_code, een verification_uri en een verification_uri_complete met de code al in de query, plus het poll-interval en een expires_in. Hij print de URI.
  2. Een persoon opent die URI terwijl hij is ingelogd, op /settings/security. Hij ziet welke agent vraagt, namens wiens account die zou werken, tot wanneer, en precies wat die zou kunnen doen. Dan kiest hij Toestaan of Niet toestaan.
  3. De agent pollt POST /mcp/auth/token met de deviceCode, en respecteert authorization_pending, slow_down, expired_token en access_denied. Bij succes krijgt hij { access_token, token_type, scope, expires_at }.

In beide gevallen is er een menselijke stap, omdat de credential de rechten van een persoon erft. Een persoon moet er dus voor kiezen om ze uit te lenen. Hier wordt niets uit het niets toegekend. Koppelen kan ook voor een hele organisatie worden uitgeschakeld op /admin/security?tab=programs, en dat antwoord wordt gecontroleerd wanneer iemand een verbinding toestaat, niet wanneer een agent er een start.

Ontdekking

Een niet-geauthenticeerde POST /mcp antwoordt met 401 en een WWW-Authenticate-header die naar /.well-known/oauth-protected-resource wijst (RFC 9728). Dat document noemt deze origin als de autorisatieserver. De metadata op /.well-known/oauth-authorization-server (RFC 8414) noemt de endpoints voor registratie, autorisatie, token en intrekking. Die keten is alles wat een client nodig heeft zodra hij het adres heeft. Om het adres vanuit een domein te vinden, leest een agent /.well-known/ai-catalog.json, dat verwijst naar de Server Card op /mcp/server-card (MCP Server Cards, SEP-2127). De endpoints van de device grant staan in het document van de beschermde bron onder x-nessie-device-authorization.

Een deployment kan ook op een tweede, korter adres antwoorden. https://nessie.works/mcp van Nessie Cloud wordt doorgestuurd naar de API, en een client die zo binnenkomt krijgt dat adres terug als resource. Inloggen blijft op de API. Een redirect zou niet volstaan, omdat een client metadata weigert die een ander adres noemt dan het adres dat hij kreeg. De beheerder zet die origins in NESSIE_MCP_ALIAS_ORIGINS.

Scopes en tools

Scopes zijn bewust grof: een scope die niemand kan uitleggen, zet niemand goed. Een verbinding die meer dan één team bereikt, somt ze op met nessie_team_list, en elke andere tool noemt dan zijn teamId.

ScopeBereikt
boards_readProjecten, borden en taken, met hun opmerkingen, bestanden, labels, typen en checklists
boards_writeTaken aanmaken, bewerken en verplaatsen, en alles wat erbij hoort
documents_readKennisruimten, documenten en spreadsheets
documents_writeZe opstellen en bewerken, en vragen om er één te publiceren
clients_readDe klantinbox, contacten, contactlijsten, nieuwsbrieven en contactformulieren
clients_writeNotities, gesprekken overnemen, toewijzen en sluiten, contactlijsten en het voorbereiden van nieuwsbrieven
clients_sendAlles wat een klant ontvangt: een antwoord, een eerste e-mail of een sms. Wordt nooit aangevinkt tenzij de persoon het zelf aanvinkt
website_toolsDe paginatools van de geregistreerde website van je team, bediend in een gehoste browser die je zelf kunt overnemen. Wordt nooit aangevinkt tenzij de persoon het zelf aanvinkt

Een aanroep zonder de scope die hij nodig heeft, komt terug als toolresultaat met de naam van de scope die gevraagd moet worden, niet als transportfout. Zo kan het model iets bruikbaars melden in plaats van telkens tegen dezelfde muur aan te lopen.

Er is geen aparte familie tools voor externe trackers. Een bord is een bord, of zijn taken nu in Nessie zijn ontstaan of uit Linear, Jira, GitHub of Trello zijn gespiegeld. Eén toolset dekt dus beide, en elke taak zegt waar hij vandaan komt. Wat verschilt, is schrijven: een bord dat is gespiegeld uit een alleen-lezen bron, weigert een schrijfactie met een tekstuele uitleg. Die zegt dat het andere systeem het ticket beheert en dat opnieuw proberen niet helpt.

Een document publiceren is een goedkeuring, nooit een scope

Agents stellen op; een persoon publiceert. Die regel wordt voor de eigen agents van Nessie afgedwongen door een agentactor volledig te weigeren. Maar een gekoppelde credential wordt als persoon opgelost, dus die weigering kan dat niet tegenhouden. De poort moet ergens anders zitten.

Daarom publiceert nessie_doc_publish niet zelf. Het opent een knowledge.page.publish-goedkeuring — dezelfde die een agent van Nessie opent — en geeft { status: "awaiting_approval", approvalId } terug. Opnieuw pollen geeft hetzelfde verzoek terug in plaats van een tweede te openen. Het besluit wordt genomen zoals elk ander publicatiebesluit in Nessie: door een persoon, over dat document, op het moment dat het bestaat.

Er is bewust geen publicatiescope. Die was er wel: een vinkje bij het koppelen dat zei dat deze agent mocht publiceren. Dat is teruggetrokken. Het beantwoordde, één keer en voor negentig dagen, een vraag die het product overal elders per document stelt, en het deed dat voordat het document bestond. De persoon die het vinkte, kon dus niet weten waarmee hij instemde. nessie_doc_publish vereist documents_write, omdat vragen niet gratis is: het leest de pagina en legt een besluit voor aan iemand. Maar die scope verleent niets over publicatie.

Twee identiteiten worden op het verzoek vastgelegd, en het onderscheid is dragend. De aanvrager is de credential, nooit de mens namens wie die handelt. De vereiste goedkeurder is die mens, en alleen die persoon mag antwoorden. Als het verzoek de persoon als aanvrager had genoemd, zou de regel die voorkomt dat iemand zijn eigen verzoek goedkeurt, precies de enige persoon uitsluiten die gerechtigd is te beslissen.

Zien en intrekken wat er gekoppeld is

De gekoppelde agents van een persoon staan op /settings/security, met een label en een intrekkingsknop voor elk. Een eigenaar of beheerder van de organisatie ziet elke credential in de organisatie op /admin/security?tab=programs: hoe elk heet, wat het mag, of het nog geldig is en wiens account het leent. Die kan elk ervan intrekken. De persoonlijke lijst blijft alleen voor jezelf zichtbaar: een lijst van geldige credentials is een lijst van aanknopingspunten, geen algemene leesstof.

Wijzigingen die via een credential worden gedaan, worden toegeschreven aan de mens die het leende, en dragen het id van de credential in de auditmetadata. Zo kan het logboek een eigen bewerking van een persoon onderscheiden van een bewerking die zijn agent voor hem deed.

Wat nog niet beschikbaar is

Expliciet, want de kloof tussen de mogelijkheden van een protocol en die van een bepaalde server is de plek waar documentatie meestal faalt:

  • Chat, kanalen, runs en agentbeheer zijn niet via MCP beschikbaar.
  • Een programma stelt een nieuwsbrief op en een persoon verstuurt hem. Niets verstuurt, plant of verwijdert een campagne, en de e-mail zelf wordt in de ontwerper geschreven.
  • Contacten koppelen en samenvoegen, wijzigingen aan het CRM-schema, imports en kanaalinstellingen blijven bij een persoon in de browser.
  • De koppelschakelaar is een instelling op organisatieniveau. Overrides op team- en persoonsniveau kan een team of persoon vandaag niet instellen.