Neo Chat plugins expose executable tools to compatible model providers. A plugin can come from an OpenAPI manifest, a built-in definition, or a remote streamable HTTP MCP server. Enabled plugin functions are sent to the model as tools, and runtime tool calls execute through server routes. Plugins are different from Skills: Skills are text-only prompt-context instructions stored locally, while plugins and MCP servers are network-capable tools executed by the server-side plugin route.
Plugins use the Plugin and PluginFunction interfaces from
src/lib/plugin/types.ts.
Required plugin fields:
| Field | Purpose |
|---|---|
id |
Stable plugin id used by settings, registry lookup, and tool execution. |
title |
User-facing plugin name. |
description |
User-facing summary. |
logoUrl |
Logo URL shown in the plugin market. |
manifestUrl |
URL for the source manifest or OpenAPI document. |
functions |
Tool functions exposed by the plugin. |
Optional fields include externalDocsUrl, baseUrl, source, mcp,
category, categories, added, builtIn, and auth. Existing OpenAPI
plugins may omit source; built-ins use builtin, imported OpenAPI plugins
use openapi, and MCP-backed plugins use mcp.
Plugin IDs must be stable. Built-in plugin IDs are reserved; a custom plugin or manifest import cannot replace a built-in tool definition.
Each function should define:
| Field | Purpose |
|---|---|
name |
Tool name. Keep it stable and model-friendly. |
description |
Short description sent to the model. |
parameters |
JSON-schema-like parameter object. |
path |
Relative request path for REST/OpenAPI tools. Absolute URLs and protocol-relative paths are rejected. |
method |
HTTP method for REST/OpenAPI tools, usually GET, POST, PUT, PATCH, or DELETE. |
mcpToolName |
Original remote MCP tool name. MCP functions omit path and method. |
risk |
Optional risk level: read, write, destructive, or external. |
If risk is omitted, Neo Chat infers it from the HTTP method: GET maps to
read, DELETE maps to destructive, and other non-GET methods map to
write. MCP tools should use external because the side effects are owned by
the remote server.
MCP support is intentionally folded into the existing plugin system. Installed
MCP servers live in installedPlugins, enabled MCP servers live in
activePlugins, and credentials live in pluginConfigs using the same BYOK
local-secret path as OpenAPI plugins. There is no separate activeMcpServers
store.
Version 1 supports only remote streamable-http MCP servers discovered from
the official MCP Registry. It does not launch local stdio processes, npm
packages, Docker containers, or OAuth login flows. MCP server URLs are
HTTPS-only. Local and self-hosted deployments may call localhost or private
network HTTPS endpoints for LAN MCP servers; hosted deployments block those
targets unless ALLOW_LOCAL_NETWORK_PROXY=true is set.
During installation, the server route opens a short-lived MCP SDK client,
calls listTools, converts the tools into PluginFunction entries, registers
the resulting plugin in the server registry, and returns it to the browser for
local installation. Local tool names use a deterministic format:
mcp_<server_slug>__<sanitized_tool_name>
Names are capped at the chat tool-schema limit and get a short hash suffix
when truncation or same-plugin collisions occur. The model sees only the local
tool name. Execution maps it back through plugin.mcp.toolNameMap or
function.mcpToolName, then calls MCP callTool({ name, arguments }).
MCP results are returned through the same /api/plugins/execute response
shape as REST plugin results and are compacted before storage if they exceed
plugin execution limits.
Registry metadata can provide static remote headers, which are stored in
plugin.mcp.headers and sent with MCP listTools and callTool requests.
Registry secret or required header metadata is mapped to the existing plugin
auth UI. If a server requires auth before listTools, installation returns a
clear auth-required error until a pre-install credential flow is added.
Plugin auth supports:
nonebearerapiKeybasicoauth2
For API keys, set name and in (header or query) when the upstream API
requires a specific key location. User-entered plugin secrets are stored as
local BYOK envelopes before server routes use them.
OpenAPI conversion supports a bounded subset:
- The spec must be a JSON object with a
pathsobject. - A server URL or OpenAPI
hostmust be present. - Supported methods are
GET,POST,PUT,PATCH, andDELETE. - Plugin paths must start with
/, must not start with//, and are truncated to the configured path limit. - Query and path parameters are converted into tool parameters.
- Operation names come from
operationIdwhen available, with unsafe characters converted to underscores. - The importer caps the number of paths, parameters, and plugin functions to prevent oversized manifests.
Hosted mode blocks legacy payloads where the browser submits a complete plugin definition for execution. In hosted deployments, plugin execution must resolve through server-registered plugin ids and function names.
Set shared registry storage for hosted or multi-instance deployments:
DEPLOYMENT_MODE=hosted
PLUGIN_REGISTRY_STORE=upstash
UPSTASH_REDIS_REST_URL=https://...
UPSTASH_REDIS_REST_TOKEN=...Built-in plugins are always resolvable by ID. Custom plugins should be registered before use and stored in the shared registry for hosted or multi-instance deployments; otherwise another instance may be unable to resolve the function call.
Built-in media plugin IDs are reserved and protocol-specific. Agnes and Gemini
image tools are image processing plugins, openai-image-generation targets the
OpenAI-compatible Images API, and openai-responses-image-processing targets
the OpenAI Responses API. Supported built-ins can expose plugin-level API Base
URL and Model ID fields; Agnes video remains a two-step create_video /
get_video_result flow and accepts public HTTPS image URLs for image-to-video.
Runtime tool calls execute automatically after a plugin is enabled for the chat. There is no per-call confirmation modal, so the plugin market, function toggle, auth configuration, and risk metadata are the user's control points.
If two active plugins expose the same function name, execution returns a collision error instead of choosing one silently. Keep function names unique across plugins that users are likely to enable together.
- Keep plugin
baseUrland OpenAPI server URLs on trusted HTTPS origins for hosted deployments. - Prefer
GETfor read-only tools and reserve mutating HTTP methods for actions that actually change external state. - Mark destructive or external-side-effect functions with explicit risk metadata.
- Keep descriptions concise and specific so the model can choose tools correctly.
- Avoid function-name collisions with other built-in or commonly installed plugins.
- Do not log plugin secrets, provider keys, or raw private user data.
Relevant checks:
pnpm test -- src/__tests__/pluginConfig.test.ts
pnpm test -- src/__tests__/pluginManifest.test.ts
pnpm test -- src/__tests__/pluginResolve.test.ts
pnpm test -- src/__tests__/serverPluginRegistry.test.ts
pnpm test -- src/__tests__/mcpRegistry.test.ts
pnpm test -- src/__tests__/mcpInstallRoute.test.ts
pnpm test -- src/__tests__/mcpExecuteRoute.test.tsRun the full project checks before opening a pull request:
pnpm format:check
pnpm lint
pnpm typecheck
pnpm test
pnpm build