Agentic Assistant
FreeMaintainedAn AI assistant that acts inside your apps with the user's own permissions.
About
An AI assistant that acts inside your apps with the user's own permissions.
README
An AI assistant that acts inside your apps with the user's own permissions.
Status: 0.7.7, early. The core loop, the tool sources, approvals, the React panel, the CLI, the MCP server, and the hosted HTTP API work.
A demo Worker runs at https://agentic-assistant-demo.hello-fd1.workers.dev. GET / shows a demo page and GET /health answers 200. The /api/v1 routes need a bearer key, so without one they answer 401. The demo has no model key set. A signed-in POST /api/v1/chat answers 503 with not_configured. The demo never fakes an answer.
Install
npm install agentic-assistant
You need Node.js 22 or newer. The React panel needs react 18 or newer.
To build from source:
git clone https://github.com/pooriaarab/agentic-assistant && cd agentic-assistant
npm ci && npm run build
Quick start
import { AnthropicProvider, ToolRegistry, createPrincipal, runAssistant } from "agentic-assistant";
const registry = new ToolRegistry().register({
name: "notes_read",
description: "Read one note by id",
inputSchema: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
scope: "notes:read",
risk: "safe", // "risky" and "irreversible" tools wait for a person
handler: async ({ id }) => ({ text: `note ${id}` }),
});
// The host decides the scopes. The assistant never widens them.
const principal = createPrincipal({
id: "agent-1",
kind: "agent_of_human",
parentId: "user-42",
scopes: ["notes:read"],
});
for await (const event of runAssistant({
// Reads ANTHROPIC_API_KEY. Model claude-sonnet-5-5. useDefaultEndpoint calls a US-hosted API: pass baseUrl for an EU one.
provider: new AnthropicProvider({ useDefaultEndpoint: true }),
registry,
principal,
messages: [{ role: "user", content: [{ type: "text", text: "Read note 7." }] }],
limits: { maxTurns: 6, maxTotalTokens: 50_000 },
})) {
if (event.type === "text_delta") process.stdout.write(event.text);
}
Load tools from an MCP server
import { connectMcpTools } from "agentic-assistant";
const mcp = await connectMcpTools({
transport: { type: "stdio", command: "npx", args: ["-y", "some-notes-mcp-server"] },
// The host decides the scope of each tool, by name. There is no default. Do not trust
// `tool.annotations`: the server writes them. A tool that is not in the list is skipped.
scope: (tool) => {
const scope = { notes_read: "notes:read", notes_write: "notes:write" }[tool.name];
if (scope === undefined) throw new Error("tool is not on the host's list");
return scope;
},
// The host also says how risky each tool is. There is no default.
risk: (tool) => (tool.name === "notes_read" ? "safe" : "risky"),
});
registry.registerAll(mcp.tools); // mcp.skipped lists tools that were not loaded, with the reason
// ...run the assistant, then:
await mcp.close();
An HTTP server works the same way: { type: "http", url: "https://tools.example.eu/mcp", headers: { authorization: "Bearer ..." } }.
Ask a person before a risky action
import { QueueApprovalGate } from "agentic-assistant";
const approvals = new QueueApprovalGate();
for await (const event of runAssistant({ provider, registry, principal, messages, approvals })) {
if (event.type === "approval_request") {
showToPerson(event.request); // tool, risk, exact arguments
// when the person clicks, from any handler:
// approvals.approve(event.request.id, "user-42")
// approvals.reject(event.request.id, "user-42", "not now")
}
}
An approval covers one request: the tool, the exact arguments, and the acting principal. The request carries a digest of those three. The decision must return the same digest, or the call is refused. The tool runs on a copy of the exact arguments the person saw, so a change to the original object cannot reach it. The agent cannot approve its own action. A gate that fails, a missed deadline (approvalTimeoutMs, default five minutes), or a wrong answer means the call is refused. To connect your own approval service, implement ApprovalGate: one method that returns a decision.
Add the side panel
import { AssistantPanel } from "agentic-assistant/react";
import "agentic-design/tokens.css";
import "agentic-assistant/react.css";
<AssistantPanel
title="Notes assistant"
// Run the assistant where your app wants: in the page, or behind your own HTTP stream.
run={(messages, signal) => runAssistant({ provider, registry, principal, messages, approvals, signal })}
// Pass a person's choice to your approval gate. Your app knows who the person is.
decide={(id, approved, reason) => (approved ? approvals.approve(id, userId) : approvals.reject(id, userId, reason))}
/>;
The panel colors are agentic-design tokens (--agd-color-*), so load agentic-design/tokens.css once, as above. The panel also reads the --agd-* space, radius, type, control-size, and shadow tokens from the same file. Each one has a fallback. The panel then follows your light or dark theme. Without the tokens the panel uses system colors, and buttons show no hover change. Set all --agd-color-* tokens or none: a partial set can give a pair that fails contrast. To set your own colors, override the --aa-* variables on .agd-assistant. If you override --aa-bg or --aa-accent, also set --aa-hover-bg and --aa-accent-hover.
The panel streams the answer, shows each tool call as a card, and shows an approval prompt with the exact arguments. Every message, card, and prompt that the assistant makes carries a visible "AI-generated" label. Tool output is shown as text and never as HTML. The panel does not hold a model, a key, or a gate. It needs react 18 or newer.
The look comes from plain CSS variables (--aa-bg, --aa-fg, --aa-accent, and others). Set them on .agd-assistant. The sheet loads no font, image, or file from another host. A prompt that the run left open (after Stop or an error) is closed and marked expired. Closing the panel stops the run.
Use the CLI
export ANTHROPIC_API_KEY=...
agentic-assistant chat \
--tools https://notes.example.eu/mcp --tools-header-env authorization=NOTES_TOKEN \
--scope notes:read --tool-scope notes:read --tool-risk safe \
--use-default-endpoint \
"What does note 7 say?"
The assistant prints its answer on stdout. The answer starts with Assistant (machine-made):. Tool calls and approval requests on stderr start with [machine-made], and so does the question that --approve ask shows. Control characters are removed from all text it prints for a person, so a tool cannot move your cursor or change your terminal title. --json events are not changed: they carry the actor with machineMade: true on each action, and the text events belong to the labeled run. Add --json to get one event per line on stdout: that is the form for agents and tests. The exit code is 0 when the run ends normally, 1 when it stops for another reason (a cap, an error), and 2 for a usage error.
--toolstakes anhttpsMCP URL, or a command that starts a server on stdio (words split on spaces, or a JSON list like'["node","server.js"]').--scopeis what the assistant may hold.--tool-scopeis what each tool needs. Both are required, and the CLI guesses neither.--tool-riskdefaults torisky. A risky call needs approval. Approve one exact call ahead of time with--approve-call '{"tool":"notes_write","input":{"id":"7","text":"hi"}}'. The call must match the tool and the arguments, and each approval is used once. On a terminal,--approve askasks you.- Keys come from the environment:
ANTHROPIC_API_KEY, or the variable named by--api-key-env. There is no flag that takes a key. - With
ANTHROPIC_API_KEY, or the variable named by--api-key-env, unset or empty,mcpprints one line on stderr that names the variable, writes nothing to stdout, and exits 2. - For an EU-hosted model, use
--provider openai-compatible --base-url https://models.example.eu/v1 --model <id> --api-key-env EU_KEY. Add--price-inand--price-outto use the cost cap. - A message on stdin may be up to 1 MB.
- Limits:
--max-turns,--max-tokens,--max-cost,--max-tool-calls. --provider scripted --script turns.jsonreplays fixed turns. It is for tests.
Run it as an MCP server
agentic-assistant mcp --tools https://notes.example.eu/mcp --scope notes:read --tool-scope notes:read --tool-risk safe --use-default-endpoint
Add this to an MCP client as a stdio server. Without --api, it offers one tool, assistant_chat, with one argument, message. Each call starts a new conversation. The reply is JSON with machineMade: true, the answer, the stop reason, and the tool calls. A run that stops on a cap is an error result. The caps (--max-turns, --max-cost) apply to one call, and the server has no total limit, so set a limit in the client that calls it. The server stops when the client closes its input. A call that is still running is cancelled and dropped: the client gets no reply for it. The tool servers are closed. The server cannot ask a person, so a risky call runs only when you started the server with a matching --approve-call.
Tools and approvals from the CLI and MCP
A running assistant API holds the calls that wait for a person. Four CLI commands reach it. The token comes from an environment variable, never from an argument.
export ASSISTANT_TOKEN=...
agentic-assistant approvals --api https://assistant.example.eu --api-token-env ASSISTANT_TOKEN
agentic-assistant approve <id> --digest <digest> --api https://assistant.example.eu --api-token-env ASSISTANT_TOKEN
tools lists the tools, approvals prints the id and digest of each waiting call, approve needs that digest so you approve the exact call you read, and deny <id> --reason ... is final. Add --json for JSON. Exit codes: 0 done, 1 the API refused or failed, 2 bad usage.
Start the MCP server with --api and --api-token-env and it also offers assistant_tools, assistant_approvals, and assistant_deny. They act with the token you gave. assistant_approvals returns the arguments of each call quoted as data. assistant_approve is off unless you add --allow-approve. An AI client could use it to approve a call in the name of the token's owner. So use a token for an agent, which needs the assistant:admin scope and is recorded as machine-made. Never give it a person's token.
Run it as a hosted HTTP API
// src/index.ts of your Worker. `store`, `registry`, and `Env` are yours.
import { createWorker } from "agentic-assistant/worker";
import { AnthropicProvider } from "agentic-assistant";
import { createStoreAuth } from "agentic-identity";
let worker: ReturnType<typeof createWorker> | undefined;
export default {
fetch(request: Request, env: Env) {
// Build the worker once. It keeps the pending approvals, so a new one per request would lose them.
worker ??= createWorker({
auth: createStoreAuth(store(env)),
registry,
provider: () => new AnthropicProvider({ baseUrl: env.MODEL_URL, apiKey: env.MODEL_KEY }),
});
return worker.fetch(request, env);
},
};
To deploy the demo Worker yourself, set the secrets ASSISTANT_TOKENS (a JSON map of a bearer token of 32 to 512 characters from A-Za-z0-9_- to {"id":"ana","scopes":["demo:read"]}) and MODEL_KEY, set the var MODEL_URL (or MODEL_USE_DEFAULT_ENDPOINT=1 to call the US-hosted api.anthropic.com), then run npm run deploy:demo. Without MODEL_KEY, chat answers 503. agentic-assistant/worker also exports createStaticTokenAuth, an agentic-identity Auth for a fixed map of bearer tokens. The demo Worker requires the scope demo:read on every route (a 403 without it): add it to each ASSISTANT_TOKENS entry when you upgrade. In your own Worker, pass routeScopes to createWorker to ask for a scope per route group.
Every route is under /api/v1 and needs Authorization: Bearer <key>. The key resolves to an agentic-identity principal. A person, an agent, and a token that acts for a user all work. A missing, bad, or odd credential is a 401. An Auth that fails gives a 503. A write signed in with a session cookie needs an Origin listed in the origins option, or it is a 403. Every chat response has the header x-ai-generated: true.
| Route | What it does |
|---|---|
GET /tools |
The tools inside the caller's scopes: name, description, scope, risk, input schema. |
POST /chat {"message":"..."} |
One new conversation. The answer is a stream of server-sent events (event: <type>, data: <json>), the same events as runAssistant, ending with done. |
GET /approvals |
Pending risky calls of the same owner, with the digest and canDecide. |
POST /approvals/:id/approve {"digest":"..."} |
Approve the exact call. A wrong digest is a 409. |
POST /approvals/:id/deny {"reason":"..."} |
Refuse the call. |
The assistant holds the caller's scopes and no more. A chat turn waits at a risky call until another principal decides. The stream stays open for that time. A principal that asked for the turn never decides on its own proposal: an agent gets a 403 for both approve and deny. The same holds for an agent it made and for the agent that made it. A person who asks may decide, because the assistant proposed the call. Another agent may decide only when it holds assistant:admin and the scope of the tool. A principal with another owner sees a 404. The list hides a call whose tool the caller has no scope for. If a client hangs up, the run stops and its pending calls go. If the key is revoked or its scopes change during a run, the run stops before its next tool call.
Use the HTTP client
agentic-assistant/client is a typed client for the hosted API routes. It uses only fetch, so it runs in Workers and Node. In a browser it works on the same origin only, because the API sends no CORS headers.
import { createClient, AssistantApiError } from "agentic-assistant/client";
const assistant = createClient({ baseUrl: "https://assistant.example.com", token });
const tools = await assistant.tools();
for await (const event of assistant.chat("Delete note n1")) {
if (event.type === "text_delta") process.stdout.write(event.text);
}
try {
const [pending] = await assistant.approvals();
if (pending?.canDecide) await assistant.approve(pending.id, pending.digest);
} catch (error) {
if (error instanceof AssistantApiError) console.error(error.code, error.status);
}
- The methods match the routes:
health,tools,chat,approvals,approve,deny. Each takes an optional{ signal }as its last argument. chatis an async generator of the same events asrunAssistant, ending withdone. It sends the request on the first read. A run that fails on the server ends with anerrorevent. A stream that ends with nodoneorerrorthrowsUNKNOWN: the run did not finish. Stop reading to stop the run. The stream stays open while a risky call waits, so decide from another call.baseUrlis the Worker URL:https, orhttponlocalhost,127.0.0.1, or[::1], with no query, hash, or credentials. A path prefix is for a host that strips it before the Worker. Passfetchto use your own.healthsends no token.- A failed call throws
AssistantApiError.codeisINVALID_INPUT,UNAUTHENTICATED,FORBIDDEN,NOT_FOUND,METHOD_NOT_ALLOWED,CONFLICT,RATE_LIMITED,NOT_CONFIGURED,INTERNAL, orUNKNOWN.statusis the HTTP status.retryAfteris set for a 429. A200reply with the wrong shape fails asUNKNOWN. - The client never follows a redirect and never retries. A decision that you abort after the client sends it can still apply: read
approvals()before you try again.
AI transparency
The package marks what the assistant makes as AI-generated. This follows the idea of EU AI Act Article 50.
Each tool result and each finished run carries a provenance record: generator: "ai", the model, the provider, the time, the assistant id, the person it acts for, and the tool, its risk, and the approver when someone approved the call. The approver has a kind: human, agent, host (a rule you set), or unknown. Say "a person approved" only for human. After a fallback, the record names the model that answered. A denied call has no record, because nothing ran.
import { provenanceLabel, provenanceMarker } from "agentic-assistant";
for await (const event of runAssistant(options)) {
if (event.type === "tool_result" && event.provenance) {
provenanceLabel(event.provenance); // "AI-generated by model-1 (acme), approved by ana"
provenanceMarker(event.provenance); // JSON-LD with the IPTC trainedAlgorithmicMedia source type
}
}
The record is in the SDK events, in the HTTP chat stream, in --json output of the CLI, and in the assistant_chat result of the MCP server. The MCP result also holds label and marker. The CLI prints the label after each run.
The host must still do these things:
- Store the marker with every document or file the assistant changes.
- Show the label to people where they read the content.
- Label chat text that the assistant makes, if you show it outside this package.
- Keep records as long as the law needs, and get legal advice. This package does not make a deployment compliant.
The record says who made the content. It does not say the content is true.
Features
- Model port.
ModelProviderhas two adapters.AnthropicProviderneeds abaseUrl, oruseDefaultEndpoint: trueto callapi.anthropic.com, which is a US-hosted endpoint. There is no silent default.OpenAICompatibleProvidercalls any server that speaks the chat-completions stream, so you can use an EU-hosted model. Both adapters need anhttpsURL, except forlocalhost. If you pass your ownclienttoAnthropicProvider, you own its endpoint. The OpenAI-compatible adapter needs an API key for any other host and cuts a response at a size limit. - Scoped tools. Each tool names the scope it needs, for example
notes:write. Scopes use the same<resource>:<action>format asagentic-identity. The model sees only the tools inside the scopes. The loop refuses a call to any other tool. - Tool sources. Load tools from an MCP server over stdio or HTTP, or write typed SDK tools with
defineTool. The loader skips a tool with a bad name, schema, or scope. It keeps text output and leaves images and other content out. It cuts long output, long descriptions, and long text inside schemas. It skips a duplicate name, a schema that is too large, and a schema with a regular expression (pattern). It stops at a page limit, a load time, and a size limit on each HTTP response. - Input validation. The loop checks each call against the tool's JSON Schema before the handler runs. Nothing is coerced. A bad call returns an error to the model and never reaches the tool.
- Tool output is data. Each result is quoted in
<tool_output>tags. Every<in the output is escaped. Every request carries a notice that tool output is never an instruction. This lowers the risk of prompt injection. It does not remove it: keep tools narrow and scopes small. - Approvals. Every tool has a required
risk:safe,risky, orirreversible. The loop asks theApprovalGatebefore a risky or irreversible call. With no gate, those tools are not offered and are refused. The text a person writes when they decline goes to the event, not to the model. - React panel.
agentic-assistant/reacthasAssistantPanelanduseAssistant. It is built to these WCAG 2.2 AA points: named controls, a live conversation log, keyboard use, focus that moves to an approval prompt and then to the message box, buttons that are at least 32 px, a visible focus ring, and a color palette checked for contrast. - CLI and MCP server.
chatandmcptake the host choices as flags and refuse to guess a scope. - Fixed principal. The assistant always acts as
agent_of_human.createPrincipalthrows when the kind is missing or different. - Caps. A max-turn cap, a total-token cap, and a cost cap. A cap that trips stops the run. No tool runs after that. A tool result is cut at
maxToolResultChars. A request that is sure to pass the token cap is not sent. The cost cap also limits the output of each request. A turn with more thanmaxToolCallsPerTurncalls (default 16) runs none of them. - Stops that fail closed. An expired or revoked principal stops the run. So does a revocation check that throws or returns anything but
false. The loop checks the principal again at the start of each run. A provider error ends the run withreason: "provider_error"and keeps the transcript. Truncated or refused model output never runs its tool calls. - Machine-made labels. Every tool call, result, and the end of each run carry an
actorwithmachineMade: true. - Test provider.
agentic-assistant/testingexportsScriptedProvider. It replays a fixed script for E2E tests.
Limits
- The hosted API keeps pending approvals in the memory of one Worker instance. A decision must reach the instance that holds the chat stream. Run it on one instance or behind sticky routing until a shared queue exists. It has no per-IP limit, only a cap of four chat turns at once per principal.
- The tests run the API under Node with a fake model. They do not run it under
wrangler dev. The demo Worker was checked by hand underwrangler devand on workers.dev. - Workers refuse code generation, so there a tool schema is checked by a small built-in checker, not by Ajv. It supports type, properties, required, additionalProperties, enum, const, items, length and size bounds, and anyOf, oneOf, allOf. It accepts annotation keywords such as
descriptionandformatbut does not checkformat, as the Ajv setup does not. It refuses a tool whose schema has any other keyword, such as$ref. - The CLI reads one message, or a short conversation on a terminal. It does not keep a conversation between runs.
- The panel is tested in a simulated browser (jsdom). Contrast is checked from the CSS values. Run a screen reader and a real browser before you ship it to people who need it.
- The panel shows plain text, not Markdown.
- The gate trusts the name that
decidedBycarries. Your gate must check who the person is.QueueApprovalGatedoes not. - An MCP server is trusted for its own output only. The assistant does not sandbox the server process. Run stdio servers you trust.
- Token and cost caps need the provider to report usage. The OpenAI-compatible adapter throws when the server sends none.
- A cost is an estimate from the
pricingyou give. Cache discounts are not applied. The built-in Anthropic prices were checked on 2026-10-29 (PRICING_CHECKED_ON). A test fails when that date is over 120 days old. Haiku has no built-in price, because its rate changes with the prompt size. Passpricingfor it. Check the prices before you rely on a cap. - The token pre-check guesses three characters per token. It is an estimate.
- A tool error text goes to the
detailfield of the event. The model sees onlytool failed. - A model refusal stops the run, unless you set a fallback model.
FallbackProvider(primary, fallback)or--fallback-model <id>asks the second model once when the first refuses or fails. Eachturn_endevent hasansweredBywith the model that answered. The text of the first model is held until its turn ends, so a turn with a fallback does not stream word by word. A refusal from the fallback stops the run. The fallback gets only the output room that the first ask left, and the caps count the input of both asks, so a token or cost cap holds. A first ask that failed after it streamed is counted by an estimate. If the fallback itself fails after it streamed, the totals of that run leave out the first ask. Its error text is not kept.
Account export and erase
import { createMemoryStore, type PrincipalContext } from "agentic-identity";
import { assistantAccount } from "agentic-assistant/account";
import { createAccountRegistry } from "agentic-identity/account";
const registry = createAccountRegistry().register(assistantAccount, undefined);
const subject = { id: "ana", kind: "human" } as const;
const store = createMemoryStore(); // Use the host identity store in production.
declare const actor: PrincipalContext; // Use the signed-in actor from the host.
const data = await registry.export(subject);
const result = await registry.erase(subject, store, { actor });
// data.manifest.apps is [{ id: "assistant", files: [] }].
// result.counts.assistant is {}.
The contributor implements AccountPackage<void> from agentic-identity 0.10.
It holds no persistent account data. Both calls check the subject id and kind.
An invalid subject stops the call. A repeat erase returns empty counts.
The runtime sends messages, tool results, and machine-made provenance to the host. It stores no items, versions, history, sharing grants, attachments, or blobs. Pending approvals live in memory during a run. Stop active runs and cancel pending approvals before account erasure. This contributor does not stop runs or clear host memory.
The host must register the packages that store its data. Those packages must export owned and authored data, delete sole-owned items, and keep other owners on shared items. They must handle damaged records and retries. Retained agent outputs must keep their machine-made labels. Empty counts here cover only this package; they do not confirm that the host erased the account. Credentials and identity audit records belong to the identity account flow.
Develop
npm ci
npm run ci:local
The live smoke test calls the real Anthropic API. It costs a few cents, so it runs only when you ask:
AGENTIC_ASSISTANT_LIVE=1 ANTHROPIC_API_KEY=... npx vitest run src/live.smoke.test.ts
License
MIT.
Install Agentic Assistant in Claude Desktop, Claude Code & Cursor
unyly install agentic-assistantInstalls into Claude Desktop, Claude Code, Cursor & VS Code — handles npx, uvx and build-from-source repos for you.
First time? Get the CLI: curl -fsSL https://unyly.org/install | sh
Or configure manually
Run in your terminal:
claude mcp add agentic-assistant --env AGENTIC_ASSISTANT_LIVE="" --env ANTHROPIC_API_KEY="" --env ASSISTANT_TOKEN="" -- npx -y agentic-assistantStep-by-step: how to install Agentic Assistant
FAQ
Is Agentic Assistant MCP free?
Yes, Agentic Assistant MCP is free — one-click install via Unyly at no cost.
Does Agentic Assistant need an API key?
Yes, it requires environment variables: AGENTIC_ASSISTANT_LIVE, ANTHROPIC_API_KEY, ASSISTANT_TOKEN. Unyly injects them into the config during install.
Is Agentic Assistant hosted or self-hosted?
Self-hosted: the server runs locally on your machine via the install command above.
How do I install Agentic Assistant in Claude Desktop, Claude Code or Cursor?
Open Agentic Assistant on unyly.org, pick your client tab (Claude Desktop, Claude Code, Cursor) and press Install — the config is generated automatically, no JSON editing.
Changes
Versions and requested access over time.
- New version published
Related MCPs
Opencode Omniroute Plugin
OpenCode plugin for the OmniRoute AI Gateway. Drives dynamic model discovery, /connect auth flow, and multi-instance OmniRoute providers via the official @openc
by GitHub ActionsRoblox Studio
Enables AI coding tools to control Roblox Studio for workspace exploration, instance manipulation, and script management. It provides tools for playtesting, sce
by paralovAntigravity Drawio
MCP server that enables AI assistants to create, parse, render, and validate Draw.io diagrams programmatically.
by S-SUJAN-SFetch
Web content fetching and conversion for efficient LLM usage.
AWS KB Retrieval
Retrieval from AWS Knowledge Base using Bedrock Agent Runtime.
by modelcontextprotocolSpring AI MCP Server
Provides auto-configuration for setting up an MCP server in Spring Boot applications.
llm-analysis-assistant
A very streamlined mcp client that supports calling and monitoring stdio/sse/streamableHttp, and can also view request responses through the /logs page. It also
by xuzexin-hzMCP-Agent
A simple, composable framework to build agents using Model Context Protocol by [LastMile AI](https://www.lastmileai.dev)
by lastmile-aiSpring AI MCP Client
Provides auto-configuration for MCP client functionality in Spring Boot applications.
mcp.natoma.ai
A Hosted MCP Platform to discover, install, manage and deploy MCP servers by [Natoma Labs](https://www.natoma.ai)
Compare Agentic Assistant with
Not sure what to pick?
Find your stack in 60 seconds
Author?
Embed badge for your README
Browse similar
All ai MCPs
