Skip to main content

Manifest Reference

Every extension folder contains a neatcontext-extension.json manifest that identifies the extension, declares how it authenticates, and tells NeatContext how to launch its MCP server.

Example

{
"id": "acme-logs",
"name": "Acme Logs",
"version": "0.1.0",
"description": "Read-only log search against the Acme log platform.",
"publisher": "Your Name",
"connection": {
"kind": "api_key",
"label": "Acme Logs (read)",
"fields": [
{ "key": "apiKey", "label": "API key", "secret": true },
{ "key": "baseUrl", "label": "Base URL", "secret": false, "required": false, "placeholder": "https://logs.acme.internal" }
],
"config": { "apiVersion": "v2" }
},
"mcpServer": {
"transport": "stdio",
"command": "node",
"args": ["./server.cjs"],
"requiresConnection": true
},
"allowed_profiles": []
}

Top-level fields

id (string, required)

Stable, unique identifier: lowercase letters, numbers, and hyphens (e.g. acme-logs). Used to track the extension, to store its connection, and in the OAuth redirect URI — keep it stable across versions. Ids of bundled first-party extensions (pagerduty, datadog) cannot be reused.

name (string, required)

Human-readable name shown in the Extensions UI and in connection messages ("Connect Acme Logs").

version, description, publisher (strings)

Shown to users deciding whether to enable the extension. Bump version when you ship changes.

connection (object)

How the extension authenticates — see Connection kinds. Omitting it means { "kind": "none" }.

mcpServer (object)

How NeatContext launches the extension's MCP server.

FieldTypeMeaning
transportstringOnly "stdio" is supported: the server is spawned and exchanges Content-Length-framed JSON-RPC over stdin/stdout.
commandstringExecutable to run. "node" is special-cased: it runs on NeatContext's bundled Node runtime, so users don't need Node installed. Other commands (python, a native binary) must exist on the user's machine.
argsstring[]Arguments, e.g. ["./server.cjs"]. Relative paths resolve against the extension folder.
requiresConnectionbooleanWhen true, NeatContext resolves the user's connection and pushes the neatcontext/connection handshake right after initialize on every tool call. Use false (the default) with connection: none.

The server is spawned fresh for each tools/list and each tools/call — keep it stateless, and make tools/list work without credentials.

allowed_profiles (string[])

Restricts the extension to specific domain profiles by id. An empty array means no restriction.

Connection kinds

none

"connection": { "kind": "none" }

No authentication and no connection UI. Use for local systems or endpoints the server reaches without user credentials. Guide: Build Your First Extension.

api_key and bearer

"connection": {
"kind": "api_key",
"label": "Acme Logs (read)",
"fields": [
{ "key": "apiKey", "label": "API key", "secret": true },
{ "key": "baseUrl", "label": "Base URL", "secret": false, "required": false, "placeholder": "https://logs.acme.internal" }
],
"config": { "apiVersion": "v2" }
}

The user pastes credentials into an inline form generated from fields. Secret values are encrypted with the OS secure storage; required fields are validated before saving, with errors shown inline on the card. The two kinds are mechanically identical — pick the one that describes the credential. Guide: API-Key Extensions.

Each entry in fields:

PropertyTypeDefaultMeaning
keystring— (required)Property name your server reads from the payload.
labelstringkeyInput label; also used in validation messages.
secretbooleantruetrue: password input, encrypted at rest, delivered in the payload's values. false: plain input, delivered in config.
requiredbooleantruefalse allows the field to be blank; blank optional fields are omitted from the payload and the server applies its own default.
placeholderstringthe labelHint shown in the empty input.

oauth2_pkce

"connection": {
"kind": "oauth2_pkce",
"label": "Acme Tickets (read)",
"authorizeUrl": "https://identity.acme.com/oauth/authorize",
"tokenUrl": "https://identity.acme.com/oauth/token",
"clientId": "your-oauth-client-id",
"scopes": ["read"],
"redirectPort": 48981,
"config": { "apiBaseUrl": "https://api.acme.com" }
}

NeatContext runs the whole Authorization Code + PKCE flow from these parameters: browser consent, localhost callback, token exchange, encrypted storage, and automatic refresh before each tool call. Guide: OAuth Extensions.

PropertyTypeMeaning
authorizeUrlstring (required)The provider's authorization endpoint.
tokenUrlstring (required)The provider's token endpoint (code exchange and refresh).
clientIdstring (required)Your OAuth app's client id (a public PKCE client — no secret is stored).
scopesstring[]Scopes to request; prefer the narrowest read-only set.
redirectPortnumber (required)Fixed localhost port for the callback. The derived redirect URI — register it with the provider exactly — is http://127.0.0.1:<redirectPort>/oauth/<id>/callback.

Shared: label and config

  • label (string) — display label for the connection.
  • config (object of strings) — static, non-secret configuration merged into the payload's config (API base URL, API version, account settings). Keeps environment specifics out of your server code. Never put secrets here — the manifest is plain text.

The connection handshake

When mcpServer.requiresConnection is true and the user is connected, NeatContext pushes one notification right after initialize:

{
"jsonrpc": "2.0",
"method": "neatcontext/connection",
"params": {
"extensionId": "<your id>",
"connection": {
"kind": "api_key | bearer | oauth2_pkce",
"values": { "…secret fields, or accessToken for OAuth…": "…" },
"config": { "…manifest config + non-secret fields (+ scope for OAuth)…": "…", "diagnosticLogPath": "…" }
}
}
}
  • values — the decrypted secrets: your secret fields keyed by key (api_key/bearer), or { "accessToken": "…" } (oauth2_pkce, refreshed right before injection).
  • config — the manifest's config, plus the user's non-secret field values (api_key/bearer) or the granted scope (oauth2_pkce), plus a diagnosticLogPath your server may append troubleshooting logs to (redact secrets).

If the user is not connected, the notification never arrives — your tools should detect that and return a connection-required result:

Terminal results

A tool result (returned as JSON in your text content) can tell NeatContext to stop the tool loop and hand the problem to the user instead of letting the model retry:

Result shapeEffect
{ "connection_required": true, "provider": "<id>", "message": "…" } (plus your own error code)Loop stops; the connected AI client relays message as a "connect it first" prompt instead of guessing. Use when credentials are missing or rejected.
{ "error": "<provider>_access_denied", "provider": "<id>", "message": "…", "diagnostic_log": "…" }Loop stops with the message (and the log path, if given). Use for permission failures while connected.
{ "error": "…", "message": "…" } without those markersNot terminal — the model sees the error and may correct itself (fix a query, choose another tool). Use for ordinary failures.

Tool naming

Tool names from user extensions must not start with neatcontext_ — that prefix is reserved for bundled first-party extensions, and prefixed tools from user extensions are filtered out. Use your own short prefix (acme_search_logs).

Packaging notes

  • The manifest and everything the server needs must live in the same folder you add to NeatContext; the folder is copied as-is into NeatContext's own data directory. Keep it lean — don't ship node_modules/ (prefer Node built-ins and the global fetch), build outputs, or logs.
  • Keep secrets out of the manifest and out of the folder entirely. User credentials are collected by NeatContext and injected per call; anything else the server needs should come from connection.config or the environment.