Pico AI Bridge
Enables interaction with Ollama-compatible chat and model APIs, including chat generation with options, model listing, and model capability inspection.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Pico AI Bridgelist all models on my Pico AI server"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Pico AI Bridge (Unofficial)
English | Japanese
Unofficial project. This bridge is not affiliated with or endorsed by Starling Protocol, the developer of Pico AI Server (formerly Pico AI Homelab).
Pico AI Bridge is a local stdio MCP server that lets Claude call models on your Pico AI Server. It uses readable JavaScript, Node.js built-ins, no runtime packages, and no build step.
Requirements
Pico AI Server on an Apple Silicon Mac, reachable from the machine running Claude. The Claude Desktop extension requires its URL at install; the Claude Code plugin defaults to
http://localhost:11434.Claude Code plugin or manual configuration: Node.js 20+ on
PATH.Claude Desktop
.mcpb: uses Desktop's built-in Node.js.
Related MCP server: ccg-mcp
Tools
Tool | Use |
| Check connectivity and return version, name, build, and bundle identifier. |
| Merge |
| Responses API generation and function calls. |
| Chat completions generation. |
| Ollama chat generation and options. |
| Opt-in sequential comparison of two or more models. |
In default mode, every Pico response field used to extract or compute a result must have its expected JSON type when present and non-null; malformed fields return unexpected_response with the endpoint message. Absent or null optional fields remain null. The same rule applies to comparison rows. Raw mode returns parsed Pico bodies after API key redaction; if JSON contains a non-finite number or unsafe integer, it returns the original text after redaction. Tool-call argument JSON with an unsafe integer stays as text. Default mode reports response_parse_failed for such a 2xx body, and Pico HTTP errors keep the original text.
Every tool accepts raw: true. A single endpoint returns structuredContent: {status, body}: the HTTP status and parsed Pico body, or text when the body is not JSON. The text content contains the serialized body, or the body itself when it is a string. The configured API key is redacted in Pico body values and keys. Pico HTTP errors return {source: "pico", status, body} with isError: true. Raw pico_list_models returns v1_models and api_tags status/body pairs and, with include_capabilities, api_show keyed by the name sent to /api/show; any Pico HTTP error sets isError: true. Raw pico_compare_models returns a successful model's status, body, and elapsed_ms. A failed comparison row contains error and comparison continues; the outer result has no isError.
pico_list_models merges /v1/models and /api/tags by the Ollama model-name rule: a name without a tag means :latest. Each default-mode entry has id, created, and owned_by from /v1/models data, name and tag_model from /api/tags, display_name from the /v1/models models array, status from status.value, and format, family, parameter_size, quantization_level, size_bytes, and modified_at from /api/tags; size_gb is bytes divided by 1e9 and rounded to two decimals (missing values are null). With include_capabilities, the bridge calls /api/show once per merged model, using its /api/tags name or its id if absent there. A successful show adds capabilities and context_length; a Pico HTTP error adds capabilities_error: {source, status, body}. The result includes partial_errors items shaped {path, error: {source, status, body}} for listing endpoint HTTP errors. If both listing endpoints fail, it returns {models: [], partial_errors} with isError: true. A connection failure or timeout returns a bridge error. Successful malformed lists return unexpected_response: /v1/models needs data objects with string id and, when present, a status object whose value is a string when present and non-null (an absent or null value extracts as null), and an array of objects in models when present; /api/tags needs models objects with string name; size, when present, must be a finite number. In default mode, a successful non-object /api/show body, non-string capabilities array entries, a non-array capabilities, a non-object model_info, or a non-finite or nonnumeric .context_length returns unexpected_response. Absent or null optional fields remain null. Raw listing without capabilities does not validate list shapes. Raw listing with include_capabilities checks only that each 2xx /v1/models body has a data array of objects with string id, and each 2xx /api/tags body has a models array of objects with string name, so it can call /api/show; it does not validate status, size, or other fields.
For generation, chat completions require prompt, messages, images, or system alone; extra.messages also supplies input when none of those is given. Ollama accepts these inputs or omits messages entirely; supplied message items are sent as given. Comparison accepts exactly one of prompt or messages, or images alone, and sends a supplied messages array as Responses input when api: "responses". Comparison system becomes Responses instructions; images attach as described below. For chat and Ollama generation, system prepends a system message. With system alone, the request contains only that message. Responses accepts optional input as a string or an array of items sent as given, plus optional instructions; it has no messages argument. When input is absent, the bridge omits it and passes Pico's result through. images attaches to the last user message: string content becomes text and image parts, array content gets image parts appended, and Ollama appends base64 strings to that message's images. An entry starting with data:, http://, or https:// (ASCII case-insensitive, no leading whitespace) is a URI; anything else is a local file path. ~ and paths starting with ~/ are resolved against the home directory of the bridge process; other paths are used as given (relative paths resolve against its working directory). After classification, every Ollama data URI is parsed as a URL, removing ASCII tabs and newlines and trimming surrounding C0 controls and spaces; its fragment is then removed before finding the first comma. The payload after that comma is percent-decoded to bytes. If the trimmed media type ends in ; followed by optional spaces and then base64 (case-insensitive), those bytes are decoded with forgiving-base64, which accepts ASCII whitespace and optional padding. Ollama sends standard base64 of the resulting bytes. A percent sign without two ASCII hex digits remains a literal byte. Invalid URLs, missing commas, and invalid base64 return invalid_arguments. Image URLs pass through. With images and no other input, including an empty messages or input array, the bridge sends one user message containing only images. Numeric tool arguments require finite numbers and safe integers. A non-finite number or an integer above Number.MAX_SAFE_INTEGER in magnitude anywhere in the arguments, including inside extra, messages, tools, or options, is rejected because JSON would lose its value; invalid values return invalid_arguments before requests. Other target content shapes return invalid_arguments before image reads. With Ollama images, an existing non-array images field on the target user message also returns invalid_arguments before image reads. Local paths are read without checking image type; image URLs are not fetched. Chat response_format, Responses json_schema with required json_schema_name (as text.format: {type: "json_schema", name: json_schema_name, schema: json_schema}), and Ollama format are sent to Pico; the bridge does not check answers against schemas. Responses previous_response_id is sent as given. tools is sent in each API's format. All POST tools send stream: false; extra.stream conflicts. Other extra keys conflict when the tool already sends that field; conflicts return invalid_arguments before image reads or HTTP requests. The generation model argument is optional, and omission leaves model selection to Pico. GET tools accept neither extra nor split_thinking.
Default chat completions returns the first choice's answer, refusal (from message.refusal, or null), reasoning, response_model, finish_reason, response_id, created, usage, and tool_calls with {id, type, name, arguments} for function items or {id, type, name, input} for custom items; custom input stays free text. Responses concatenates all output_text parts and all refusal part strings in output order and returns answer, refusal (null when absent), reasoning, response_model, status, response_id, created_at, completed_at, usage including cached_tokens, error, incomplete_details, and function_calls with {id, call_id, name, arguments, status}, plus custom_tool_calls with {id, call_id, name, input, status}. Both call arrays are always present, and absent custom-call id and status become null. Ollama returns message.content as answer, plus reasoning, response_model, finish_reason, count usage, durations_ms, created_at, done, and tool_calls with {id, name, arguments}. Absent values are null where those fields are provided. A successful default comparison row has model, answer, reasoning, response_model, API-specific tool_calls, or function_calls and custom_tool_calls for Responses, finish_reason or status, response_id and created for chat, response_id, created_at, and completed_at for Responses, or durations_ms, created_at, and done for Ollama, refusal for chat and Responses, usage when Pico returns it (always {prompt_eval_count, eval_count} for Ollama, with nulls for absent counts), and elapsed_ms. Responses comparison rows also include error and incomplete_details, each null when absent from Pico. Failed comparisons have a per-row error. After a sent request times out, comparison waits for Pico to finish it before sending the next model, even when the concurrency limit is above one; MCP cancellation ends that wait and stops the comparison. Default generation and comparison check 2xx bodies: chat needs a nonempty choices array whose first item has a message with string role; its content may be a string (including an empty string), null, or absent, and null or absent content gives an empty answer. Chat refusal may be a string, null, or absent. Chat reasoning_content and reasoning may each be a string, null, or absent. Chat tool_calls may be an array, null, or absent; each array item needs string id and type. For type: "custom", it needs an object custom with string name and free-text string input; every other type keeps the object function with string name and string arguments rule. Ollama needs a message with string role and string content (including an empty string); thinking may be a string, null, or absent. Ollama durations (total_duration, load_duration, prompt_eval_duration, eval_duration) must be finite numbers when present; null and absent values remain null. Ollama tool_calls may be an array, null, or absent; each array item must be an object with an object function and string function.name; function.arguments may be absent, null, an object, or a string (absent or null extracts as null). Responses needs an output array of objects with string type; each item's content and summary may be arrays, null, or absent, and each array part must be an object with a string type; its text may be a string, null, or absent, except output_text, summary_text, and reasoning_text parts require string text, and a refusal part requires string refusal. A function_call item requires string name, call_id, and arguments. A custom_tool_call item requires string name, call_id, and free-text input; its optional id and status must be strings when present. Unused fields, later chat choices, semantic relationships, and the contents of argument strings or objects are not checked. Empty answers are valid.
POST tools default to split_thinking: true: an orphan closing </think> splits preceding text into reasoning, then paired <think>...</think> blocks are removed from the answer and joined into reasoning. split_thinking: false keeps inline text in the answer; separate Pico reasoning fields are still used. Responses reasoning items use nonempty summary, otherwise content. reasoning_effort is passed as reasoning_effort in chat and reasoning.effort in Responses. Ollama think accepts a boolean. extra.think sends a level string or null unchanged; the tested Pico server accepted null and rejected level strings. In testing, Claude Desktop chat (with Sonnet 5.5 and Opus 5.5) sent true as the string "true" when think also accepted strings, while Claude Code sent a boolean. Comparison's reasoning applies to chat (reasoning_effort) and Responses (reasoning.effort); think and options apply only to Ollama. Using an API's inapplicable argument returns invalid_arguments. In an Ollama comparison, options.num_predict conflicts with max_tokens, and options.temperature conflicts with temperature; these also return invalid_arguments.
In the tested Pico server, replaying an Ollama tool call needed an assistant message with tool_calls items containing an id, type: "function", and string function.arguments, followed by a tool message with a matching tool_call_id. That server rejected replays of its own id-less calls. For Chat Completions, the bridge parses valid JSON tool_calls[].arguments into a JSON value; replay the assistant message with function.arguments as a JSON string and add a matching tool message, for example messages: [{role: "assistant", tool_calls: [{id: "call_1", type: "function", function: {name: "get_weather", arguments: "{\"city\":\"Tokyo\"}"}}]}, {role: "tool", tool_call_id: "call_1", content: "{\"temperature_c\":20}"}]. For Responses, replaying the user item, the function_call item, and a function_call_output item worked; sending only function_call_output with previous_response_id was rejected. The tested server returned HTTP 400 with Coding key `function` not found. for Chat custom tools; Responses accepted the custom tool definitions but returned message output without custom calls. The tested server also returned HTTP 400 for an Ollama format JSON schema object and for Chat function tool definitions without description, although the Ollama and OpenAI API references document both as valid. On the tested server, Ollama think: true returned no thinking text and no inline <think> text for any tested model, while LFM2.5-1.2B-Thinking-4bit with think: false wrote inline <think> text that split_thinking moves into reasoning. Other servers may behave differently.
Bridge errors use {source: "bridge", kind, message}; Pico HTTP errors keep their status and parsed body. The configured API key is redacted after extraction in Pico-derived values and in keys of Pico objects returned as-is, including raw and error bodies, usage, Responses error and incomplete_details, tool-call arguments, model fields, and api_show keys. Bridge-built field names stay fixed. The bridge does not follow redirects. pico_server_info includes bundle_identifier and returns null for absent name, version, build, or bundle identifier and does not infer API support from the version; a successful non-object /api/version body returns unexpected_response.
Install
Claude Code marketplace
/plugin marketplace add takuomi/pico-ai-bridge
/plugin install pico-ai-bridge@pico-ai-bridgePlugin zip
Download pico-ai-bridge-<version>-plugin.zip from Releases, then open Customize > Plugins > Upload plugin. When Claude Code v2.1.273 or later is signed in with the same Claude account, the plugin syncs to it each time Claude Code starts; it does not sync when Claude Code uses an API key or a cloud provider such as Amazon Bedrock. Uploading does not make its local server available in claude.ai web chat.
Claude Desktop extension
Download pico-ai-bridge-<version>.mcpb from Releases and double-click it, or use Settings > Extensions > Advanced settings > Install Extension.
Manual MCP configuration
For Claude Code:
claude mcp add pico-ai-bridge --env PICO_BASE_URL=http://localhost:11434 -- node /absolute/path/to/pico-ai-bridge/server/index.jsFor Claude Desktop, add to claude_desktop_config.json:
{
"mcpServers": {
"pico-ai-bridge": {
"command": "node",
"args": ["/absolute/path/to/pico-ai-bridge/server/index.js"],
"env": { "PICO_BASE_URL": "http://localhost:11434" }
}
}
}Surface support
Surface | Support |
Claude Desktop chat | Yes through |
Claude Code CLI and desktop Code tab | Yes through plugin or manual config; Node.js 20+ required. |
Cowork | Not yet supported. |
claude.ai web and mobile chat | Not supported; local MCP servers are not loaded there. |
Configuration
Plugin option / environment variable | Default | Meaning |
| Claude Desktop extension: required at install (no default). Claude Code plugin: | Pico server URL. Do not include credentials in the URL; use API key. |
| empty | Optional Bearer token for authentication. |
|
| Caller timeout for the request and body reading; integer range: 1 through 2147483647 ms. |
|
| FIFO limit on HTTP requests to Pico. Loading several large models concurrently can exhaust a Mac's memory. |
A queued request that times out is removed without being sent to Pico. An individual request returns its timeout error promptly. A timeout after sending leaves the request holding its FIFO permit until Pico finishes and its body is read; a comparison waits for that request to finish before starting the next model, so the comparison may return later than the individual timeout. MCP cancellation ends the comparison wait promptly; cancellation removes queued requests only. Requests already sent are never aborted. Closing a connection while Pico was generating was followed by the server stopping on the next generation request in testing.
See Privacy Policy.
A missing, empty, or unsubstituted PICO_BASE_URL is rejected on every tool call with invalid_configuration. Configure base_url in the extension or plugin, or set PICO_BASE_URL for manual use.
A malformed base_url, one using a protocol other than HTTP or HTTPS, or one containing credentials, a query, or a fragment is rejected on every tool call with invalid_configuration. A timeout outside its integer range or a concurrency limit that is not a safe integer of at least 1 also returns invalid_configuration; empty or unsubstituted values use the defaults.
Development
npm test runs the sorted test/*.test.js files, and npm run test:conformance runs the sorted dev/*.test.js files. Both commands fail if their directory has no listed files. test/tools-contract.json records the six public tool definitions checked by the unit and adapter conformance tests. npm run test:live runs test/live.js against PICO_BASE_URL. In CI, scripts/smoke-artifacts.py extracts each archive, substitutes the manifest's declared command, arguments, and environment values, then runs initialize and pico_server_info against a local mock Pico on Linux, macOS, and Windows. It does not install either archive in Claude Desktop or Claude Code.
npm test
npm ci --prefix dev
npm run test:conformance
npm run check:size
PICO_BASE_URL=http://localhost:11434 npm run test:liveLive tests use PICO_TEST_TOOLS_MODEL and PICO_TEST_VISION_MODEL (both default to Qwen3-VL-2B-Instruct-4bit) and PICO_TEST_THINKING_MODELS (a comma-separated list, default LFM2.5-1.2B-Thinking-4bit). deepseek-r1-distill-qwen-1.5b-4bit is not a default because on the tested server its /api/chat replies sometimes ran for about two minutes and ended without a message. Other servers may behave differently. At least two distinct role models are required. All distinct models run generation through the three APIs in default and raw modes with temperature 0 and a 4096-token limit. The live test also starts the shipped server/index.js stdio server, sends a legacy initialize and calls each of the six tools with integer IDs, including default and raw generation through all three APIs, checks each result, and checks for exit code 0 after stdin closes. It also uses 2026-07-28 per-request metadata without initialize to discover the server, list all six tools, and call each of the three generation tools through the shipped stdio server. Comparisons run sequentially with temperature 0 and a 3072-token limit (default and raw modes for all three APIs). Additional default comparisons for all three APIs use messages: [{role: "user", content: prompt}] with temperature 0 and a 1024-token limit; every row must have an answer and no error. A default chat comparison attaches the generated red PNG to the vision model and tools model (or repeats the vision model when they match) with the same settings and row checks. Temperature 0 keeps small thinking models from sometimes running away. The live test also checks GET tools, model merge and capabilities, tool calls and tool-result round trips, JSON formats, system instructions, extra, keep_alive, previous_response_id, and image content shapes. Structured-output formats, keep_alive, and previous_response_id are live acceptance checks: they show Pico accepted the request, while unit tests assert the exact outgoing fields. Tool-call, vision, and thinking behavior is asserted directly on the assigned role model without capability gating or retries. Vision uses a temporary generated red PNG. Prompted calls expect the answer to name red; image-only calls require a nonempty answer. Responses reasoning_effort is checked with low, which must succeed, and with an invalid value, which must return Pico HTTP 400, showing that the value reaches Pico as reasoning.effort. Thinking tests use Ollama think:true and think:false for each thinking model. For think:false, the assertions check bridge invariants on real output: no complete <think>...</think> pair remains in the answer when splitting, and split_thinking: false leaves the answer unsplit with empty reasoning. The tests do not require the model to write inline reasoning. An unknown model must return Pico HTTP 404 through all three APIs, with identical error data in raw and default modes and a body matching the direct Pico response. PICO_TEST_BROKEN_MODEL optionally selects another model expected to return a Pico error; all three APIs are checked in raw and default modes against a direct Pico HTTP response. A default chat comparison then checks that this error stays in its row and the tools model succeeds. A separate comparison uses a 1000 ms timeout and concurrency of 2. A local forwarding proxy records chat request arrival and response completion times and checks that the next model request arrives after the timed-out response finishes. The test then checks server information and a short chat.
SDK v2 dependencies and conformance tests live only in dev/. The same official v2 client assertions run against the shipped adapter and the SDK adapter in legacy, modern-pinned, and auto-detect modes. Modern clients use 2026-07-28 per-request metadata; server/discover and modern -32022 errors list only 2026-07-28 for subsequent requests. Legacy clients use initialize to negotiate one of the four legacy revisions. Each request uses its own _meta to select modern handling. A request whose params._meta is present but not an object receives -32602 in either era. A legacy request without modern metadata needs a successful legacy initialize on the connection, except ping; legacy initialize remains available after modern requests. Request IDs must be strings or safe integers; invalid IDs receive -32600 with a null ID. Modern MCP removed ping; the conformance suite verifies it only in legacy mode. After a successful stdio initialize for 2025-03-26 or 2024-11-05, a nonempty JSON-RPC batch gets one response array if any element needs a reply, omitting notifications and cancelled requests; an all-notification or all-cancelled batch writes nothing, and an empty batch gets one -32600 error object; an initialize inside a batch gets -32600 for that element, and each modern-era request element gets its own -32600 (modern notifications get no reply). Array lines remain invalid before initialize, with newer legacy revisions, and for modern per-request handling. See Architecture.
Pico AI Server and its name are trademarks of their respective owner. This project is independent and MIT licensed.
This server cannot be deployed
Maintenance
Related MCP Connectors
Use AI models for chat, image, and video generation from Claude Code and other MCP hosts.
- QuallaaOAuthcom.quallaa
Talk to your public-facing AI from any MCP client — Claude, ChatGPT, Cursor, Cline, Windsurf.
A comprehensive Model Context Protocol (MCP) server that enables AI assistants to interact with yo…
Real-time chat hub for AI agents — Claude Code, Cursor, Cline, Codex over MCP or REST.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA bridge that enables Claude Code to interact with local Ollama instances for text generation, multi-turn chat, and vision-based analysis. It supports model management tasks such as listing, pulling, and showing details, alongside generating text embeddings.363 npm1MIT
- AlicenseNot gradedqualityDmaintenanceEnables Claude Code Desktop to call OpenAI Codex/GPT-5 and Gemini APIs via stdio MCP, without requiring local CLI tools.MIT
- FlicenseNot gradedqualityCmaintenanceMCP server enabling Claude chat, image analysis, live web search, and model listing through Anthropic's Messages API. Allows any MCP client to consult Claude as a tool via stdio JSON-RPC.-
- AlicenseNot gradedqualityBmaintenanceBridges LM Studio local models to Claude Code as MCP tools, enabling private and free AI-assisted subtasks. Includes respond and models tools with automatic model selection and structured output.MIT