Vibecode Composio
track this build4 phases, 12 steps, beginner friendly0%If you personally use four apps, not four hundred, most of Composio evaporates. An agent can write you a local MCP server that wraps Gmail, Slack, GitHub and Notion with your own personal access tokens in about a session, and the tool-calling loop works fine. What you are actually buying from Composio is the part nobody enjoys: OAuth app registration per provider, token refresh, per-user credential storage, rate limit handling, and schemas for tools you have not thought of yet. That gets ugly the moment you want more than a handful of integrations, or multiple users, or an OAuth flow instead of a static token you pasted in. Solo and token-based: build it. Multi-tenant or breadth-hungry: pay.
You are building a lean indie version of Composio. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Composio · indie build A local MCP server exposing hand-written tools for the three or four services you actually use, where the hard parts are a transport that breaks on one stray print, credentials that must never reach the model, provider limits the agent cannot see, and write tools an impatient agent will call twice. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Protocol | The MCP SDK over stdio, with every log line on stderr | the specification reserves stdout for protocol messages | | Language | TypeScript on Node 22 with a zod schema per tool | the schema is the contract the model reads | | HTTP | One shared fetch helper with timeouts, Retry-After and a per-provider budget | each provider limits differently and the agent calls in loops | | Credentials | Personal tokens from the environment or the platform keychain, never from tool arguments | an agent that can pass a token can leak one | | Tests | A mocked-HTTP suite covering 401, 429 and repeated writes | the failure paths are the product | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An MCP-capable client** · free - Why: The stdout discipline and the tool descriptions are only testable under a client that actually speaks the protocol. - Get it: Claude Desktop, Cursor, or your own agent loop built on the SDK. - Verify: The client lists the smoke tool from phase one - [ ] **Personal tokens for your providers, least privilege, with expiry** · free - Why: Every tool is a wrapper around one of these, and a token with more permission than the tools use is exposure for nothing. - Get it: A GitHub fine-grained token with an expiry and only the repositories and permissions the tools need, a Slack user token for your own workspace, a Notion integration token shared with only the pages you want read. - Verify: Each token lists exactly the permissions the tools call - [ ] **A throwaway channel, repository and page to write into** · free - Why: Idempotency and dry run are tested by posting, and the first posts of a new write tool should land somewhere nobody reads. - Get it: A private Slack channel, a scratch repository and a scratch Notion page. - Verify: A test write appears in each and nowhere else - [ ] **Which providers, and the count under five** · free - Why: The verdict on this category is that under five hand-written integrations beat a hosted layer and above it they do not; deciding the list first keeps the build honest. - Get it: Count the integrations you would call this month and write the list down. - Verify: The list exists and has fewer than five entries ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - OAuth flows and app verification. - Credentials for other people. - Hundreds of connectors. - An HTTP transport reachable beyond your machine. - Hundreds of pre-built, pre-tested tool schemas you did not have to read API docs for - Managed OAuth: app registration, consent screens, refresh tokens, revocation - Per-end-user credential storage, which is the whole game if your agent serves anyone but you - Someone else fixing your integration when a provider ships a breaking API change - Usage observability, retries and rate limit smoothing across providers If one of those is essential to you, that is the reason to keep paying for Composio, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Composio The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tool server that hands an agent your own accounts, and nobody else's to replace Composio. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Protocol: The MCP SDK over stdio, with every log line on stderr · the specification reserves stdout for protocol messages - Language: TypeScript on Node 22 with a zod schema per tool · the schema is the contract the model reads - HTTP: One shared fetch helper with timeouts, Retry-After and a per-provider budget · each provider limits differently and the agent calls in loops - Credentials: Personal tokens from the environment or the platform keychain, never from tool arguments · an agent that can pass a token can leak one - Tests: A mocked-HTTP suite covering 401, 429 and repeated writes · the failure paths are the product ### Data model (create this before Phase 1) - providers(id, enabled, base_url, token_ref, scopes_declared, rate_policy_json) · what each token may do, written down - tools(name, provider_id, kind, input_schema, output_fields, description) · kind is read or write - calls(id, tool, at, duration_ms, status, provider_status, retry_after_s, bytes_returned) · shapes and statuses, never bodies - writes(idempotency_key, tool, at, provider_ref, result_json) · a write tool never repeats a key - budgets(provider_id, window_start, used, limit, unit) · from the provider's published limits, checked before the call - The calls log records what was asked and how the provider answered but never the content returned, because a log of everything the agent read is a second copy of your mailbox. ### Phase 1 · A server that speaks only protocol on stdout Build: Bootstrap the server with the SDK over stdio and a logger bound to stderr. At startup, check that every enabled provider has its token and exit non-zero naming the missing variable. Register one smoke tool and connect a real client. Trap: A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. Done when: The server lists its tools under a real MCP client. A test launches the server, drives a tool call and asserts every stdout line parses as a JSON-RPC message. A missing token for an enabled provider exits non-zero naming the variable, and a disabled provider's tools do not appear. Logging goes to stderr and is visible in the client's log. Do not build yet: No provider tools, budgets or write handling. ### Phase 2 · Read tools that return what the model needs Build: Read tools for your three or four providers: search messages, list issues, read a page, list channels. Each has a zod input, a one-line description, an output trimmed to named fields, a byte cap and a cursor for more. A 401 names the provider and the variable to fix, never the token. Trap: Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. Done when: Every read tool's output is under the stated byte cap, asserted in tests. Pagination works through cursors and a page fetched twice is identical. A 401 returns a message naming the provider and the environment variable without the token value, tested with a canary token that must not appear. Descriptions are one line and name the input the model must supply. Do not build yet: No budgets, retries or write tools yet. ### Phase 3 · Limits the agent cannot see, made visible Build: Per-provider budgets from the published limits, in a config file with the date you checked. Honour Retry-After on 429 exactly. Bound backoff and log it. When a call would exceed the budget, return a result saying how long to wait instead of blocking or retrying silently, and put the limit in the tool's description. Trap: For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. Done when: A 429 with Retry-After is honoured to the second, tested with a mock. Each provider has a budget in config with the date it was checked, and the tool description states it. Backoff is bounded and each retry is logged. Where a provider counts units rather than calls, such as Gmail's per-user per-minute quota in which a send costs a hundred units, the budget counts units. Do not build yet: No write tools or client packaging yet. ### Phase 4 · Write tools that cannot run twice Build: Write tools (post a message, create an issue, append a block) take a required idempotency key and a dry-run flag. The key and the result are stored before the provider is called and returned on a repeat. The writes table survives a restart. Trap: An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. Done when: Repeating a write with the same key returns the first result and makes no second provider call, tested with a mock that counts. A write without a key is rejected by the schema. Dry run returns exactly what would be sent. The writes table persists across a restart and a repeat after restart is still refused. Do not build yet: No doctor command or client packaging yet. ### Phase 5 · Handing it to a client, and where the tokens live Build: A doctor command that checks each provider's token, scopes and expiry and reports per provider. The exact configuration block for two MCP clients, referencing environment variables rather than pasting tokens. Scope documentation per provider, a scan proving no token appears in any log or output, and a README section on what is out. Trap: Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days. Done when: The doctor command reports each provider as connected, expired or missing with the fix named. The client configuration references variables and connects on a fresh machine following only the README. A canary token placed in every provider never appears in logs or tool results, scanned in a test. Expired tokens surface as a per-provider message rather than a generic failure, including a GitHub fine-grained token with the expiry you set. Do not build yet: Nothing after this. ### Out of scope (and why) - OAuth flows, consent screens and app verification, which are the part you pay a hosted layer for and which multiply per provider. - Credentials for anyone but you, since storing and refreshing other people's tokens is a security product rather than a weekend. - Hundreds of connectors, which is a maintenance treadmill; under five, written by hand, is the honest scope. - An HTTP transport reachable beyond your machine; if you expose one, the specification requires validating the Origin header and binding to localhost. ### README must contain - That stdout is the protocol channel and every log line goes to stderr, enforced by a test - That tokens come from the environment or the keychain, are never accepted as tool arguments, and never appear in output or logs - That each provider's limits are written in config with the date checked and stated in the tool description - That every write tool requires an idempotency key and a repeat returns the stored result - That Gmail has no personal token and needs an OAuth client whose refresh token expires weekly while the project is in Testing ===== AGENTS.md ===== # Agent instructions · Composio indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The MCP SDK over stdio, with every log line on stderr, TypeScript on Node 22 with a zod schema per tool, One shared fetch helper with timeouts, Retry-After and a per-provider budget, Personal tokens from the environment or the platform keychain, never from tool arguments, A mocked-HTTP suite covering 401, 429 and repeated writes. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. ===== BUILD_PLAN.md ===== # Build plan · Composio A local MCP server exposing hand-written tools for the three or four services you actually use, where the hard parts are a transport that breaks on one stray print, credentials that must never reach the model, provider limits the agent cannot see, and write tools an impatient agent will call twice. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A server that speaks only protocol on stdout The server runs under a real client and nothing but MCP messages ever reaches stdout. ### Steps 1. Bind logging to stderr and guard stdout One stray print breaks the protocol; make it impossible rather than unlikely. Files: `src/index.ts`, `src/log.ts`, `tests/stdout-clean.test.ts` 2. Fail at startup on a missing token Naming the variable, before any client connects. Files: `src/config.ts` 3. Connect a real client The exact JSON block for two clients belongs in the README from day one. Files: `README.md` ### Done when - [ ] Every stdout line is a valid protocol message - [ ] A missing token exits non-zero naming the variable - [ ] Disabled providers show no tools - [ ] Logs reach stderr and the client's log ### Watch out - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. ## Phase 2 · Read tools that return what the model needs Each read tool returns trimmed, bounded, paginated results with a description the model can act on. ### Steps 1. Trim outputs to named fields The model's context is the scarce resource. Files: `src/providers/github.ts`, `src/providers/slack.ts`, `src/providers/notion.ts` 2. Paginate with cursors and cap bytes A bounded page beats a truncated dump. Files: `src/paging.ts`, `tests/output-size.test.ts` 3. Make a 401 name the fix, not the secret Test with a canary token that must never appear. Files: `src/http.ts`, `tests/no-token-leak.test.ts` ### Done when - [ ] Outputs stay under the byte cap - [ ] Cursors page correctly - [ ] A 401 names provider and variable, never the token - [ ] Descriptions are one actionable line ### Watch out - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. ## Phase 3 · Limits the agent cannot see, made visible Provider rate limits are known before the call and returned as a wait, never discovered as a ban. ### Steps 1. Write the limits down with a date They change; the config says when you last looked. Files: `config/limits.json`, `src/budgets.ts` 2. Honour Retry-After exactly To the second. A guess is a longer wait. Files: `src/http.ts`, `tests/retry-after.test.ts` 3. Return the wait rather than retrying The agent can plan around a number; it cannot see a silent sleep. Files: `src/budgets.ts` ### Done when - [ ] Retry-After is honoured to the second - [ ] Every provider has a dated budget - [ ] Backoff is bounded and logged - [ ] Unit-based quotas count units ### Watch out - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. ## Phase 4 · Write tools that cannot run twice Sending, posting and creating happen once per intent, however many times the agent asks. ### Steps 1. Require the key in the schema The model cannot omit what the schema demands. Files: `src/writes.ts`, `src/providers/slack.ts` 2. Store before calling, return on repeat Tested with a provider mock that counts calls. Files: `src/writes.ts`, `tests/idempotent-writes.test.ts` 3. Offer dry run on every write What would be sent, verbatim, with no side effect. Files: `src/writes.ts` ### Done when - [ ] A repeated key makes no second call - [ ] Keyless writes are rejected - [ ] Dry run shows exactly what would be sent - [ ] Keys survive a restart ### Watch out - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. ## Not in this build - OAuth flows and app verification. - Credentials for other people. - Hundreds of connectors. - An HTTP transport reachable beyond your machine. ## After v1, if you want it - A per-tool audit view showing what the agent asked for this week, counts only, no bodies - Provider-side search filters passed through, which cuts result sizes further than trimming - A keychain adapter so tokens leave the environment file entirely ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Enabled providers. A missing token for any of these stops startup. PROVIDERS=github,slack,notion # Optional · secret. Fine-grained, least privilege, with an expiry. The doctor command reports when it lapses. GITHUB_TOKEN=github_pat_...redacted # Optional · secret. Your own workspace. History calls are budgeted at the published non-Marketplace rate. SLACK_USER_TOKEN=xoxp-...redacted # Optional · secret. An integration shared with only the pages the tools should see. NOTION_TOKEN=ntn_...redacted # Required. Per-provider budgets with the date each was checked. LIMITS_PATH=./config/limits.json # Required. Every read tool trims to this. The model's context is the scarce resource. MAX_RESULT_BYTES=16000 # Required. Idempotency keys and results. Must survive a restart. WRITES_DB_PATH=./data/writes.sqlite # Required. Logs go to stderr only. There is no level that writes to stdout. LOG_LEVEL=info
You are building a lean indie version of Composio. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Composio · indie build A local MCP server exposing hand-written tools for the three or four services you actually use, where the hard parts are a transport that breaks on one stray print, credentials that must never reach the model, provider limits the agent cannot see, and write tools an impatient agent will call twice. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Protocol | The MCP SDK over stdio, with every log line on stderr | the specification reserves stdout for protocol messages | | Language | TypeScript on Node 22 with a zod schema per tool | the schema is the contract the model reads | | HTTP | One shared fetch helper with timeouts, Retry-After and a per-provider budget | each provider limits differently and the agent calls in loops | | Credentials | Personal tokens from the environment or the platform keychain, never from tool arguments | an agent that can pass a token can leak one | | Tests | A mocked-HTTP suite covering 401, 429 and repeated writes | the failure paths are the product | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An MCP-capable client** · free - Why: The stdout discipline and the tool descriptions are only testable under a client that actually speaks the protocol. - Get it: Claude Desktop, Cursor, or your own agent loop built on the SDK. - Verify: The client lists the smoke tool from phase one - [ ] **Personal tokens for your providers, least privilege, with expiry** · free - Why: Every tool is a wrapper around one of these, and a token with more permission than the tools use is exposure for nothing. - Get it: A GitHub fine-grained token with an expiry and only the repositories and permissions the tools need, a Slack user token for your own workspace, a Notion integration token shared with only the pages you want read. - Verify: Each token lists exactly the permissions the tools call - [ ] **A throwaway channel, repository and page to write into** · free - Why: Idempotency and dry run are tested by posting, and the first posts of a new write tool should land somewhere nobody reads. - Get it: A private Slack channel, a scratch repository and a scratch Notion page. - Verify: A test write appears in each and nowhere else - [ ] **Which providers, and the count under five** · free - Why: The verdict on this category is that under five hand-written integrations beat a hosted layer and above it they do not; deciding the list first keeps the build honest. - Get it: Count the integrations you would call this month and write the list down. - Verify: The list exists and has fewer than five entries ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - OAuth flows and app verification. - Credentials for other people. - Hundreds of connectors. - An HTTP transport reachable beyond your machine. - Hundreds of pre-built, pre-tested tool schemas you did not have to read API docs for - Managed OAuth: app registration, consent screens, refresh tokens, revocation - Per-end-user credential storage, which is the whole game if your agent serves anyone but you - Someone else fixing your integration when a provider ships a breaking API change - Usage observability, retries and rate limit smoothing across providers If one of those is essential to you, that is the reason to keep paying for Composio, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Composio The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tool server that hands an agent your own accounts, and nobody else's to replace Composio. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Protocol: The MCP SDK over stdio, with every log line on stderr · the specification reserves stdout for protocol messages - Language: TypeScript on Node 22 with a zod schema per tool · the schema is the contract the model reads - HTTP: One shared fetch helper with timeouts, Retry-After and a per-provider budget · each provider limits differently and the agent calls in loops - Credentials: Personal tokens from the environment or the platform keychain, never from tool arguments · an agent that can pass a token can leak one - Tests: A mocked-HTTP suite covering 401, 429 and repeated writes · the failure paths are the product ### Data model (create this before Phase 1) - providers(id, enabled, base_url, token_ref, scopes_declared, rate_policy_json) · what each token may do, written down - tools(name, provider_id, kind, input_schema, output_fields, description) · kind is read or write - calls(id, tool, at, duration_ms, status, provider_status, retry_after_s, bytes_returned) · shapes and statuses, never bodies - writes(idempotency_key, tool, at, provider_ref, result_json) · a write tool never repeats a key - budgets(provider_id, window_start, used, limit, unit) · from the provider's published limits, checked before the call - The calls log records what was asked and how the provider answered but never the content returned, because a log of everything the agent read is a second copy of your mailbox. ### Phase 1 · A server that speaks only protocol on stdout Build: Bootstrap the server with the SDK over stdio and a logger bound to stderr. At startup, check that every enabled provider has its token and exit non-zero naming the missing variable. Register one smoke tool and connect a real client. Trap: A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. Done when: The server lists its tools under a real MCP client. A test launches the server, drives a tool call and asserts every stdout line parses as a JSON-RPC message. A missing token for an enabled provider exits non-zero naming the variable, and a disabled provider's tools do not appear. Logging goes to stderr and is visible in the client's log. Do not build yet: No provider tools, budgets or write handling. ### Phase 2 · Read tools that return what the model needs Build: Read tools for your three or four providers: search messages, list issues, read a page, list channels. Each has a zod input, a one-line description, an output trimmed to named fields, a byte cap and a cursor for more. A 401 names the provider and the variable to fix, never the token. Trap: Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. Done when: Every read tool's output is under the stated byte cap, asserted in tests. Pagination works through cursors and a page fetched twice is identical. A 401 returns a message naming the provider and the environment variable without the token value, tested with a canary token that must not appear. Descriptions are one line and name the input the model must supply. Do not build yet: No budgets, retries or write tools yet. ### Phase 3 · Limits the agent cannot see, made visible Build: Per-provider budgets from the published limits, in a config file with the date you checked. Honour Retry-After on 429 exactly. Bound backoff and log it. When a call would exceed the budget, return a result saying how long to wait instead of blocking or retrying silently, and put the limit in the tool's description. Trap: For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. Done when: A 429 with Retry-After is honoured to the second, tested with a mock. Each provider has a budget in config with the date it was checked, and the tool description states it. Backoff is bounded and each retry is logged. Where a provider counts units rather than calls, such as Gmail's per-user per-minute quota in which a send costs a hundred units, the budget counts units. Do not build yet: No write tools or client packaging yet. ### Phase 4 · Write tools that cannot run twice Build: Write tools (post a message, create an issue, append a block) take a required idempotency key and a dry-run flag. The key and the result are stored before the provider is called and returned on a repeat. The writes table survives a restart. Trap: An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. Done when: Repeating a write with the same key returns the first result and makes no second provider call, tested with a mock that counts. A write without a key is rejected by the schema. Dry run returns exactly what would be sent. The writes table persists across a restart and a repeat after restart is still refused. Do not build yet: No doctor command or client packaging yet. ### Phase 5 · Handing it to a client, and where the tokens live Build: A doctor command that checks each provider's token, scopes and expiry and reports per provider. The exact configuration block for two MCP clients, referencing environment variables rather than pasting tokens. Scope documentation per provider, a scan proving no token appears in any log or output, and a README section on what is out. Trap: Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days. Done when: The doctor command reports each provider as connected, expired or missing with the fix named. The client configuration references variables and connects on a fresh machine following only the README. A canary token placed in every provider never appears in logs or tool results, scanned in a test. Expired tokens surface as a per-provider message rather than a generic failure, including a GitHub fine-grained token with the expiry you set. Do not build yet: Nothing after this. ### Out of scope (and why) - OAuth flows, consent screens and app verification, which are the part you pay a hosted layer for and which multiply per provider. - Credentials for anyone but you, since storing and refreshing other people's tokens is a security product rather than a weekend. - Hundreds of connectors, which is a maintenance treadmill; under five, written by hand, is the honest scope. - An HTTP transport reachable beyond your machine; if you expose one, the specification requires validating the Origin header and binding to localhost. ### README must contain - That stdout is the protocol channel and every log line goes to stderr, enforced by a test - That tokens come from the environment or the keychain, are never accepted as tool arguments, and never appear in output or logs - That each provider's limits are written in config with the date checked and stated in the tool description - That every write tool requires an idempotency key and a repeat returns the stored result - That Gmail has no personal token and needs an OAuth client whose refresh token expires weekly while the project is in Testing ===== AGENTS.md ===== # Agent instructions · Composio indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The MCP SDK over stdio, with every log line on stderr, TypeScript on Node 22 with a zod schema per tool, One shared fetch helper with timeouts, Retry-After and a per-provider budget, Personal tokens from the environment or the platform keychain, never from tool arguments, A mocked-HTTP suite covering 401, 429 and repeated writes. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. ===== BUILD_PLAN.md ===== # Build plan · Composio A local MCP server exposing hand-written tools for the three or four services you actually use, where the hard parts are a transport that breaks on one stray print, credentials that must never reach the model, provider limits the agent cannot see, and write tools an impatient agent will call twice. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A server that speaks only protocol on stdout The server runs under a real client and nothing but MCP messages ever reaches stdout. ### Steps 1. Bind logging to stderr and guard stdout One stray print breaks the protocol; make it impossible rather than unlikely. Files: `src/index.ts`, `src/log.ts`, `tests/stdout-clean.test.ts` 2. Fail at startup on a missing token Naming the variable, before any client connects. Files: `src/config.ts` 3. Connect a real client The exact JSON block for two clients belongs in the README from day one. Files: `README.md` ### Done when - [ ] Every stdout line is a valid protocol message - [ ] A missing token exits non-zero naming the variable - [ ] Disabled providers show no tools - [ ] Logs reach stderr and the client's log ### Watch out - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. ## Phase 2 · Read tools that return what the model needs Each read tool returns trimmed, bounded, paginated results with a description the model can act on. ### Steps 1. Trim outputs to named fields The model's context is the scarce resource. Files: `src/providers/github.ts`, `src/providers/slack.ts`, `src/providers/notion.ts` 2. Paginate with cursors and cap bytes A bounded page beats a truncated dump. Files: `src/paging.ts`, `tests/output-size.test.ts` 3. Make a 401 name the fix, not the secret Test with a canary token that must never appear. Files: `src/http.ts`, `tests/no-token-leak.test.ts` ### Done when - [ ] Outputs stay under the byte cap - [ ] Cursors page correctly - [ ] A 401 names provider and variable, never the token - [ ] Descriptions are one actionable line ### Watch out - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. ## Phase 3 · Limits the agent cannot see, made visible Provider rate limits are known before the call and returned as a wait, never discovered as a ban. ### Steps 1. Write the limits down with a date They change; the config says when you last looked. Files: `config/limits.json`, `src/budgets.ts` 2. Honour Retry-After exactly To the second. A guess is a longer wait. Files: `src/http.ts`, `tests/retry-after.test.ts` 3. Return the wait rather than retrying The agent can plan around a number; it cannot see a silent sleep. Files: `src/budgets.ts` ### Done when - [ ] Retry-After is honoured to the second - [ ] Every provider has a dated budget - [ ] Backoff is bounded and logged - [ ] Unit-based quotas count units ### Watch out - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. ## Phase 4 · Write tools that cannot run twice Sending, posting and creating happen once per intent, however many times the agent asks. ### Steps 1. Require the key in the schema The model cannot omit what the schema demands. Files: `src/writes.ts`, `src/providers/slack.ts` 2. Store before calling, return on repeat Tested with a provider mock that counts calls. Files: `src/writes.ts`, `tests/idempotent-writes.test.ts` 3. Offer dry run on every write What would be sent, verbatim, with no side effect. Files: `src/writes.ts` ### Done when - [ ] A repeated key makes no second call - [ ] Keyless writes are rejected - [ ] Dry run shows exactly what would be sent - [ ] Keys survive a restart ### Watch out - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. ## Not in this build - OAuth flows and app verification. - Credentials for other people. - Hundreds of connectors. - An HTTP transport reachable beyond your machine. ## After v1, if you want it - A per-tool audit view showing what the agent asked for this week, counts only, no bodies - Provider-side search filters passed through, which cuts result sizes further than trimming - A keychain adapter so tokens leave the environment file entirely ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Enabled providers. A missing token for any of these stops startup. PROVIDERS=github,slack,notion # Optional · secret. Fine-grained, least privilege, with an expiry. The doctor command reports when it lapses. GITHUB_TOKEN=github_pat_...redacted # Optional · secret. Your own workspace. History calls are budgeted at the published non-Marketplace rate. SLACK_USER_TOKEN=xoxp-...redacted # Optional · secret. An integration shared with only the pages the tools should see. NOTION_TOKEN=ntn_...redacted # Required. Per-provider budgets with the date each was checked. LIMITS_PATH=./config/limits.json # Required. Every read tool trims to this. The model's context is the scarce resource. MAX_RESULT_BYTES=16000 # Required. Idempotency keys and results. Must survive a restart. WRITES_DB_PATH=./data/writes.sqlite # Required. Logs go to stderr only. There is no level that writes to stdout. LOG_LEVEL=info
You are building a production product version of Composio. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Composio · product brief ## Problem If you personally use four apps, not four hundred, most of Composio evaporates. An agent can write you a local MCP server that wraps Gmail, Slack, GitHub and Notion with your own personal access tokens in about a session, and the tool-calling loop works fine. What you are actually buying from Composio is the part nobody enjoys: OAuth app registration per provider, token refresh, per-user credential storage, rate limit handling, and schemas for tools you have not thought of yet. That gets ugly the moment you want more than a handful of integrations, or multiple users, or an OAuth flow instead of a static token you pasted in. Solo and token-based: build it. Multi-tenant or breadth-hungry: pay. ## Product outcome A tool server that gives an agent your four accounts with the least permission each needs, tells it how long to wait rather than getting banned, and never posts anything twice. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node 20+ - Personal access tokens for each service you want to wrap - An MCP-capable client such as Claude Desktop, Cursor or your own agent loop ## Explicit non-goals for v1 - OAuth flows and app verification. - Credentials for other people. - Hundreds of connectors. - An HTTP transport reachable beyond your machine. - Hundreds of pre-built, pre-tested tool schemas you did not have to read API docs for - Managed OAuth: app registration, consent screens, refresh tokens, revocation - Per-end-user credential storage, which is the whole game if your agent serves anyone but you - Someone else fixing your integration when a provider ships a breaking API change - Usage observability, retries and rate limit smoothing across providers ## Success criteria - Every stdout line is confirmed to be a protocol message - A canary token is confirmed absent from logs and outputs - Retry-After is confirmed honoured to the second - A repeated idempotency key is confirmed to make no second call - The doctor command is confirmed to name the fix per provider ===== BRIEF.md ===== # Build brief · Composio The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tool server that hands an agent your own accounts, and nobody else's to replace Composio. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Protocol: The MCP SDK over stdio, with every log line on stderr · the specification reserves stdout for protocol messages - Language: TypeScript on Node 22 with a zod schema per tool · the schema is the contract the model reads - HTTP: One shared fetch helper with timeouts, Retry-After and a per-provider budget · each provider limits differently and the agent calls in loops - Credentials: Personal tokens from the environment or the platform keychain, never from tool arguments · an agent that can pass a token can leak one - Tests: A mocked-HTTP suite covering 401, 429 and repeated writes · the failure paths are the product ### Data model (create this before Phase 1) - providers(id, enabled, base_url, token_ref, scopes_declared, rate_policy_json) · what each token may do, written down - tools(name, provider_id, kind, input_schema, output_fields, description) · kind is read or write - calls(id, tool, at, duration_ms, status, provider_status, retry_after_s, bytes_returned) · shapes and statuses, never bodies - writes(idempotency_key, tool, at, provider_ref, result_json) · a write tool never repeats a key - budgets(provider_id, window_start, used, limit, unit) · from the provider's published limits, checked before the call - The calls log records what was asked and how the provider answered but never the content returned, because a log of everything the agent read is a second copy of your mailbox. ### Phase 1 · A server that speaks only protocol on stdout Build: Bootstrap the server with the SDK over stdio and a logger bound to stderr. At startup, check that every enabled provider has its token and exit non-zero naming the missing variable. Register one smoke tool and connect a real client. Trap: A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. Done when: The server lists its tools under a real MCP client. A test launches the server, drives a tool call and asserts every stdout line parses as a JSON-RPC message. A missing token for an enabled provider exits non-zero naming the variable, and a disabled provider's tools do not appear. Logging goes to stderr and is visible in the client's log. Do not build yet: No provider tools, budgets or write handling. ### Phase 2 · Read tools that return what the model needs Build: Read tools for your three or four providers: search messages, list issues, read a page, list channels. Each has a zod input, a one-line description, an output trimmed to named fields, a byte cap and a cursor for more. A 401 names the provider and the variable to fix, never the token. Trap: Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. Done when: Every read tool's output is under the stated byte cap, asserted in tests. Pagination works through cursors and a page fetched twice is identical. A 401 returns a message naming the provider and the environment variable without the token value, tested with a canary token that must not appear. Descriptions are one line and name the input the model must supply. Do not build yet: No budgets, retries or write tools yet. ### Phase 3 · Limits the agent cannot see, made visible Build: Per-provider budgets from the published limits, in a config file with the date you checked. Honour Retry-After on 429 exactly. Bound backoff and log it. When a call would exceed the budget, return a result saying how long to wait instead of blocking or retrying silently, and put the limit in the tool's description. Trap: For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. Done when: A 429 with Retry-After is honoured to the second, tested with a mock. Each provider has a budget in config with the date it was checked, and the tool description states it. Backoff is bounded and each retry is logged. Where a provider counts units rather than calls, such as Gmail's per-user per-minute quota in which a send costs a hundred units, the budget counts units. Do not build yet: No write tools or client packaging yet. ### Phase 4 · Write tools that cannot run twice Build: Write tools (post a message, create an issue, append a block) take a required idempotency key and a dry-run flag. The key and the result are stored before the provider is called and returned on a repeat. The writes table survives a restart. Trap: An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. Done when: Repeating a write with the same key returns the first result and makes no second provider call, tested with a mock that counts. A write without a key is rejected by the schema. Dry run returns exactly what would be sent. The writes table persists across a restart and a repeat after restart is still refused. Do not build yet: No doctor command or client packaging yet. ### Phase 5 · Handing it to a client, and where the tokens live Build: A doctor command that checks each provider's token, scopes and expiry and reports per provider. The exact configuration block for two MCP clients, referencing environment variables rather than pasting tokens. Scope documentation per provider, a scan proving no token appears in any log or output, and a README section on what is out. Trap: Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days. Done when: The doctor command reports each provider as connected, expired or missing with the fix named. The client configuration references variables and connects on a fresh machine following only the README. A canary token placed in every provider never appears in logs or tool results, scanned in a test. Expired tokens surface as a per-provider message rather than a generic failure, including a GitHub fine-grained token with the expiry you set. Do not build yet: Nothing after this. ### Out of scope (and why) - OAuth flows, consent screens and app verification, which are the part you pay a hosted layer for and which multiply per provider. - Credentials for anyone but you, since storing and refreshing other people's tokens is a security product rather than a weekend. - Hundreds of connectors, which is a maintenance treadmill; under five, written by hand, is the honest scope. - An HTTP transport reachable beyond your machine; if you expose one, the specification requires validating the Origin header and binding to localhost. ### README must contain - That stdout is the protocol channel and every log line goes to stderr, enforced by a test - That tokens come from the environment or the keychain, are never accepted as tool arguments, and never appear in output or logs - That each provider's limits are written in config with the date checked and stated in the tool description - That every write tool requires an idempotency key and a repeat returns the stored result - That Gmail has no personal token and needs an OAuth client whose refresh token expires weekly while the project is in Testing ===== ARCHITECTURE.md ===== # Architecture · Composio ## Stack | Part | Choice | Why | | --- | --- | --- | | Protocol | The MCP SDK over stdio, with every log line on stderr | the specification reserves stdout for protocol messages | | Language | TypeScript on Node 22 with a zod schema per tool | the schema is the contract the model reads | | HTTP | One shared fetch helper with timeouts, Retry-After and a per-provider budget | each provider limits differently and the agent calls in loops | | Credentials | Personal tokens from the environment or the platform keychain, never from tool arguments | an agent that can pass a token can leak one | | Tests | A mocked-HTTP suite covering 401, 429 and repeated writes | the failure paths are the product | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Transport | The stdio server and the stderr logger. | Any transport, provided nothing but protocol reaches the channel. | | Providers | Hand-written read and write tools per service. | Any service, provided outputs are trimmed and capped. | | Budgets | Dated limits and Retry-After handling. | Any policy, provided the wait is returned rather than slept through. | | Writes | Idempotency keys stored before the call. | Not swappable for retry-and-hope, which posts twice. | | Doctor | Per-provider token, scope and expiry checks. | Any implementation, provided it names the fix. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROVIDERS` · required · Enabled providers. A missing token for any of these stops startup. - `GITHUB_TOKEN` · optional, secret · Fine-grained, least privilege, with an expiry. The doctor command reports when it lapses. - `SLACK_USER_TOKEN` · optional, secret · Your own workspace. History calls are budgeted at the published non-Marketplace rate. - `NOTION_TOKEN` · optional, secret · An integration shared with only the pages the tools should see. - `LIMITS_PATH` · required · Per-provider budgets with the date each was checked. - `MAX_RESULT_BYTES` · required · Every read tool trims to this. The model's context is the scarce resource. - `WRITES_DB_PATH` · required · Idempotency keys and results. Must survive a restart. - `LOG_LEVEL` · required · Logs go to stderr only. There is no level that writes to stdout. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Composio product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The MCP SDK over stdio, with every log line on stderr, TypeScript on Node 22 with a zod schema per tool, One shared fetch helper with timeouts, Retry-After and a per-provider budget, Personal tokens from the environment or the platform keychain, never from tool arguments, A mocked-HTTP suite covering 401, 429 and repeated writes. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. - Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days. ===== MILESTONES.md ===== # Delivery milestones · Composio Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A server that speaks only protocol on stdout The server runs under a real client and nothing but MCP messages ever reaches stdout. ### Steps 1. Bind logging to stderr and guard stdout One stray print breaks the protocol; make it impossible rather than unlikely. Files: `src/index.ts`, `src/log.ts`, `tests/stdout-clean.test.ts` 2. Fail at startup on a missing token Naming the variable, before any client connects. Files: `src/config.ts` 3. Connect a real client The exact JSON block for two clients belongs in the README from day one. Files: `README.md` ### Done when - [ ] Every stdout line is a valid protocol message - [ ] A missing token exits non-zero naming the variable - [ ] Disabled providers show no tools - [ ] Logs reach stderr and the client's log ### Watch out - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. ## M2 · Read tools that return what the model needs Each read tool returns trimmed, bounded, paginated results with a description the model can act on. ### Steps 1. Trim outputs to named fields The model's context is the scarce resource. Files: `src/providers/github.ts`, `src/providers/slack.ts`, `src/providers/notion.ts` 2. Paginate with cursors and cap bytes A bounded page beats a truncated dump. Files: `src/paging.ts`, `tests/output-size.test.ts` 3. Make a 401 name the fix, not the secret Test with a canary token that must never appear. Files: `src/http.ts`, `tests/no-token-leak.test.ts` ### Done when - [ ] Outputs stay under the byte cap - [ ] Cursors page correctly - [ ] A 401 names provider and variable, never the token - [ ] Descriptions are one actionable line ### Watch out - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. ## M3 · Limits the agent cannot see, made visible Provider rate limits are known before the call and returned as a wait, never discovered as a ban. ### Steps 1. Write the limits down with a date They change; the config says when you last looked. Files: `config/limits.json`, `src/budgets.ts` 2. Honour Retry-After exactly To the second. A guess is a longer wait. Files: `src/http.ts`, `tests/retry-after.test.ts` 3. Return the wait rather than retrying The agent can plan around a number; it cannot see a silent sleep. Files: `src/budgets.ts` ### Done when - [ ] Retry-After is honoured to the second - [ ] Every provider has a dated budget - [ ] Backoff is bounded and logged - [ ] Unit-based quotas count units ### Watch out - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. ## M4 · Write tools that cannot run twice Sending, posting and creating happen once per intent, however many times the agent asks. ### Steps 1. Require the key in the schema The model cannot omit what the schema demands. Files: `src/writes.ts`, `src/providers/slack.ts` 2. Store before calling, return on repeat Tested with a provider mock that counts calls. Files: `src/writes.ts`, `tests/idempotent-writes.test.ts` 3. Offer dry run on every write What would be sent, verbatim, with no side effect. Files: `src/writes.ts` ### Done when - [ ] A repeated key makes no second call - [ ] Keyless writes are rejected - [ ] Dry run shows exactly what would be sent - [ ] Keys survive a restart ### Watch out - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. ## M5 · Handing it to a client, and where the tokens live (production only) Anyone with the README can connect a client, and a doctor command says what is wrong before the agent does. ### Steps 1. Write the doctor command Connected, expired or missing, per provider, with the fix. Files: `src/doctor.ts` 2. Reference tokens, never paste them In the client config and in the README. Files: `README.md`, `config/clients/` 3. Scan for the canary A token that appears anywhere fails the build. Files: `tests/canary-scan.test.ts` ### Done when - [ ] Doctor reports per provider with the fix - [ ] A fresh machine connects from the README - [ ] The canary appears nowhere - [ ] Expiry surfaces per provider ### Watch out - Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days. ===== OPERATIONS.md ===== # Operations · Composio ## Backup The writes database is the only state that matters, since losing it means a repeated key is no longer recognised. Back it up with the machine; everything else is regenerated from config and tokens. ## Restore Restore and confirm the writes table is present before starting the server, since a server started against an empty table will happily repeat every write the agent retries. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Budget consumption per provider and the count of 429s. A 429 that reaches the log means a budget is wrong or the provider changed its limit, and the config's date tells you which is more likely. ## Incident checklist A token in a log or a tool result is the incident: revoke it at the provider, rotate, and add the leaked pattern to the canary scan. A duplicated write is the second kind, and it means a write path bypassed the key store. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Every stdout line is confirmed to be a protocol message - [ ] A canary token is confirmed absent from logs and outputs - [ ] Retry-After is confirmed honoured to the second - [ ] A repeated idempotency key is confirmed to make no second call - [ ] The doctor command is confirmed to name the fix per provider ## Launch constraint Do not market omitted Composio capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Enabled providers. A missing token for any of these stops startup. PROVIDERS=github,slack,notion # Optional · secret. Fine-grained, least privilege, with an expiry. The doctor command reports when it lapses. GITHUB_TOKEN=github_pat_...redacted # Optional · secret. Your own workspace. History calls are budgeted at the published non-Marketplace rate. SLACK_USER_TOKEN=xoxp-...redacted # Optional · secret. An integration shared with only the pages the tools should see. NOTION_TOKEN=ntn_...redacted # Required. Per-provider budgets with the date each was checked. LIMITS_PATH=./config/limits.json # Required. Every read tool trims to this. The model's context is the scarce resource. MAX_RESULT_BYTES=16000 # Required. Idempotency keys and results. Must survive a restart. WRITES_DB_PATH=./data/writes.sqlite # Required. Logs go to stderr only. There is no level that writes to stdout. LOG_LEVEL=info
# Composio · indie build A local MCP server exposing hand-written tools for the three or four services you actually use, where the hard parts are a transport that breaks on one stray print, credentials that must never reach the model, provider limits the agent cannot see, and write tools an impatient agent will call twice. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Protocol | The MCP SDK over stdio, with every log line on stderr | the specification reserves stdout for protocol messages | | Language | TypeScript on Node 22 with a zod schema per tool | the schema is the contract the model reads | | HTTP | One shared fetch helper with timeouts, Retry-After and a per-provider budget | each provider limits differently and the agent calls in loops | | Credentials | Personal tokens from the environment or the platform keychain, never from tool arguments | an agent that can pass a token can leak one | | Tests | A mocked-HTTP suite covering 401, 429 and repeated writes | the failure paths are the product | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Node.js 22 or newer** · free - Why: Runs the server and the test suite. - Get it: nodejs.org, or nvm install 22. - Verify: node --version prints v22 or higher - [ ] **An MCP-capable client** · free - Why: The stdout discipline and the tool descriptions are only testable under a client that actually speaks the protocol. - Get it: Claude Desktop, Cursor, or your own agent loop built on the SDK. - Verify: The client lists the smoke tool from phase one - [ ] **Personal tokens for your providers, least privilege, with expiry** · free - Why: Every tool is a wrapper around one of these, and a token with more permission than the tools use is exposure for nothing. - Get it: A GitHub fine-grained token with an expiry and only the repositories and permissions the tools need, a Slack user token for your own workspace, a Notion integration token shared with only the pages you want read. - Verify: Each token lists exactly the permissions the tools call - [ ] **A throwaway channel, repository and page to write into** · free - Why: Idempotency and dry run are tested by posting, and the first posts of a new write tool should land somewhere nobody reads. - Get it: A private Slack channel, a scratch repository and a scratch Notion page. - Verify: A test write appears in each and nowhere else - [ ] **Which providers, and the count under five** · free - Why: The verdict on this category is that under five hand-written integrations beat a hosted layer and above it they do not; deciding the list first keeps the build honest. - Get it: Count the integrations you would call this month and write the list down. - Verify: The list exists and has fewer than five entries ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - OAuth flows and app verification. - Credentials for other people. - Hundreds of connectors. - An HTTP transport reachable beyond your machine. - Hundreds of pre-built, pre-tested tool schemas you did not have to read API docs for - Managed OAuth: app registration, consent screens, refresh tokens, revocation - Per-end-user credential storage, which is the whole game if your agent serves anyone but you - Someone else fixing your integration when a provider ships a breaking API change - Usage observability, retries and rate limit smoothing across providers If one of those is essential to you, that is the reason to keep paying for Composio, and the README should say so rather than pretend.
# Build brief · Composio The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tool server that hands an agent your own accounts, and nobody else's to replace Composio. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Protocol: The MCP SDK over stdio, with every log line on stderr · the specification reserves stdout for protocol messages - Language: TypeScript on Node 22 with a zod schema per tool · the schema is the contract the model reads - HTTP: One shared fetch helper with timeouts, Retry-After and a per-provider budget · each provider limits differently and the agent calls in loops - Credentials: Personal tokens from the environment or the platform keychain, never from tool arguments · an agent that can pass a token can leak one - Tests: A mocked-HTTP suite covering 401, 429 and repeated writes · the failure paths are the product ### Data model (create this before Phase 1) - providers(id, enabled, base_url, token_ref, scopes_declared, rate_policy_json) · what each token may do, written down - tools(name, provider_id, kind, input_schema, output_fields, description) · kind is read or write - calls(id, tool, at, duration_ms, status, provider_status, retry_after_s, bytes_returned) · shapes and statuses, never bodies - writes(idempotency_key, tool, at, provider_ref, result_json) · a write tool never repeats a key - budgets(provider_id, window_start, used, limit, unit) · from the provider's published limits, checked before the call - The calls log records what was asked and how the provider answered but never the content returned, because a log of everything the agent read is a second copy of your mailbox. ### Phase 1 · A server that speaks only protocol on stdout Build: Bootstrap the server with the SDK over stdio and a logger bound to stderr. At startup, check that every enabled provider has its token and exit non-zero naming the missing variable. Register one smoke tool and connect a real client. Trap: A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. Done when: The server lists its tools under a real MCP client. A test launches the server, drives a tool call and asserts every stdout line parses as a JSON-RPC message. A missing token for an enabled provider exits non-zero naming the variable, and a disabled provider's tools do not appear. Logging goes to stderr and is visible in the client's log. Do not build yet: No provider tools, budgets or write handling. ### Phase 2 · Read tools that return what the model needs Build: Read tools for your three or four providers: search messages, list issues, read a page, list channels. Each has a zod input, a one-line description, an output trimmed to named fields, a byte cap and a cursor for more. A 401 names the provider and the variable to fix, never the token. Trap: Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. Done when: Every read tool's output is under the stated byte cap, asserted in tests. Pagination works through cursors and a page fetched twice is identical. A 401 returns a message naming the provider and the environment variable without the token value, tested with a canary token that must not appear. Descriptions are one line and name the input the model must supply. Do not build yet: No budgets, retries or write tools yet. ### Phase 3 · Limits the agent cannot see, made visible Build: Per-provider budgets from the published limits, in a config file with the date you checked. Honour Retry-After on 429 exactly. Bound backoff and log it. When a call would exceed the budget, return a result saying how long to wait instead of blocking or retrying silently, and put the limit in the tool's description. Trap: For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. Done when: A 429 with Retry-After is honoured to the second, tested with a mock. Each provider has a budget in config with the date it was checked, and the tool description states it. Backoff is bounded and each retry is logged. Where a provider counts units rather than calls, such as Gmail's per-user per-minute quota in which a send costs a hundred units, the budget counts units. Do not build yet: No write tools or client packaging yet. ### Phase 4 · Write tools that cannot run twice Build: Write tools (post a message, create an issue, append a block) take a required idempotency key and a dry-run flag. The key and the result are stored before the provider is called and returned on a repeat. The writes table survives a restart. Trap: An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. Done when: Repeating a write with the same key returns the first result and makes no second provider call, tested with a mock that counts. A write without a key is rejected by the schema. Dry run returns exactly what would be sent. The writes table persists across a restart and a repeat after restart is still refused. Do not build yet: No doctor command or client packaging yet. ### Phase 5 · Handing it to a client, and where the tokens live Build: A doctor command that checks each provider's token, scopes and expiry and reports per provider. The exact configuration block for two MCP clients, referencing environment variables rather than pasting tokens. Scope documentation per provider, a scan proving no token appears in any log or output, and a README section on what is out. Trap: Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days. Done when: The doctor command reports each provider as connected, expired or missing with the fix named. The client configuration references variables and connects on a fresh machine following only the README. A canary token placed in every provider never appears in logs or tool results, scanned in a test. Expired tokens surface as a per-provider message rather than a generic failure, including a GitHub fine-grained token with the expiry you set. Do not build yet: Nothing after this. ### Out of scope (and why) - OAuth flows, consent screens and app verification, which are the part you pay a hosted layer for and which multiply per provider. - Credentials for anyone but you, since storing and refreshing other people's tokens is a security product rather than a weekend. - Hundreds of connectors, which is a maintenance treadmill; under five, written by hand, is the honest scope. - An HTTP transport reachable beyond your machine; if you expose one, the specification requires validating the Origin header and binding to localhost. ### README must contain - That stdout is the protocol channel and every log line goes to stderr, enforced by a test - That tokens come from the environment or the keychain, are never accepted as tool arguments, and never appear in output or logs - That each provider's limits are written in config with the date checked and stated in the tool description - That every write tool requires an idempotency key and a repeat returns the stored result - That Gmail has no personal token and needs an OAuth client whose refresh token expires weekly while the project is in Testing
# Agent instructions · Composio indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: The MCP SDK over stdio, with every log line on stderr, TypeScript on Node 22 with a zod schema per tool, One shared fetch helper with timeouts, Retry-After and a per-provider budget, Personal tokens from the environment or the platform keychain, never from tool arguments, A mocked-HTTP suite covering 401, 429 and repeated writes. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider.
# Build plan · Composio A local MCP server exposing hand-written tools for the three or four services you actually use, where the hard parts are a transport that breaks on one stray print, credentials that must never reach the model, provider limits the agent cannot see, and write tools an impatient agent will call twice. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · A server that speaks only protocol on stdout The server runs under a real client and nothing but MCP messages ever reaches stdout. ### Steps 1. Bind logging to stderr and guard stdout One stray print breaks the protocol; make it impossible rather than unlikely. Files: `src/index.ts`, `src/log.ts`, `tests/stdout-clean.test.ts` 2. Fail at startup on a missing token Naming the variable, before any client connects. Files: `src/config.ts` 3. Connect a real client The exact JSON block for two clients belongs in the README from day one. Files: `README.md` ### Done when - [ ] Every stdout line is a valid protocol message - [ ] A missing token exits non-zero naming the variable - [ ] Disabled providers show no tools - [ ] Logs reach stderr and the client's log ### Watch out - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. ## Phase 2 · Read tools that return what the model needs Each read tool returns trimmed, bounded, paginated results with a description the model can act on. ### Steps 1. Trim outputs to named fields The model's context is the scarce resource. Files: `src/providers/github.ts`, `src/providers/slack.ts`, `src/providers/notion.ts` 2. Paginate with cursors and cap bytes A bounded page beats a truncated dump. Files: `src/paging.ts`, `tests/output-size.test.ts` 3. Make a 401 name the fix, not the secret Test with a canary token that must never appear. Files: `src/http.ts`, `tests/no-token-leak.test.ts` ### Done when - [ ] Outputs stay under the byte cap - [ ] Cursors page correctly - [ ] A 401 names provider and variable, never the token - [ ] Descriptions are one actionable line ### Watch out - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. ## Phase 3 · Limits the agent cannot see, made visible Provider rate limits are known before the call and returned as a wait, never discovered as a ban. ### Steps 1. Write the limits down with a date They change; the config says when you last looked. Files: `config/limits.json`, `src/budgets.ts` 2. Honour Retry-After exactly To the second. A guess is a longer wait. Files: `src/http.ts`, `tests/retry-after.test.ts` 3. Return the wait rather than retrying The agent can plan around a number; it cannot see a silent sleep. Files: `src/budgets.ts` ### Done when - [ ] Retry-After is honoured to the second - [ ] Every provider has a dated budget - [ ] Backoff is bounded and logged - [ ] Unit-based quotas count units ### Watch out - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. ## Phase 4 · Write tools that cannot run twice Sending, posting and creating happen once per intent, however many times the agent asks. ### Steps 1. Require the key in the schema The model cannot omit what the schema demands. Files: `src/writes.ts`, `src/providers/slack.ts` 2. Store before calling, return on repeat Tested with a provider mock that counts calls. Files: `src/writes.ts`, `tests/idempotent-writes.test.ts` 3. Offer dry run on every write What would be sent, verbatim, with no side effect. Files: `src/writes.ts` ### Done when - [ ] A repeated key makes no second call - [ ] Keyless writes are rejected - [ ] Dry run shows exactly what would be sent - [ ] Keys survive a restart ### Watch out - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. ## Not in this build - OAuth flows and app verification. - Credentials for other people. - Hundreds of connectors. - An HTTP transport reachable beyond your machine. ## After v1, if you want it - A per-tool audit view showing what the agent asked for this week, counts only, no bodies - Provider-side search filters passed through, which cuts result sizes further than trimming - A keychain adapter so tokens leave the environment file entirely
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Enabled providers. A missing token for any of these stops startup. PROVIDERS=github,slack,notion # Optional · secret. Fine-grained, least privilege, with an expiry. The doctor command reports when it lapses. GITHUB_TOKEN=github_pat_...redacted # Optional · secret. Your own workspace. History calls are budgeted at the published non-Marketplace rate. SLACK_USER_TOKEN=xoxp-...redacted # Optional · secret. An integration shared with only the pages the tools should see. NOTION_TOKEN=ntn_...redacted # Required. Per-provider budgets with the date each was checked. LIMITS_PATH=./config/limits.json # Required. Every read tool trims to this. The model's context is the scarce resource. MAX_RESULT_BYTES=16000 # Required. Idempotency keys and results. Must survive a restart. WRITES_DB_PATH=./data/writes.sqlite # Required. Logs go to stderr only. There is no level that writes to stdout. LOG_LEVEL=info
# Composio · product brief ## Problem If you personally use four apps, not four hundred, most of Composio evaporates. An agent can write you a local MCP server that wraps Gmail, Slack, GitHub and Notion with your own personal access tokens in about a session, and the tool-calling loop works fine. What you are actually buying from Composio is the part nobody enjoys: OAuth app registration per provider, token refresh, per-user credential storage, rate limit handling, and schemas for tools you have not thought of yet. That gets ugly the moment you want more than a handful of integrations, or multiple users, or an OAuth flow instead of a static token you pasted in. Solo and token-based: build it. Multi-tenant or breadth-hungry: pay. ## Product outcome A tool server that gives an agent your four accounts with the least permission each needs, tells it how long to wait rather than getting banned, and never posts anything twice. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Node 20+ - Personal access tokens for each service you want to wrap - An MCP-capable client such as Claude Desktop, Cursor or your own agent loop ## Explicit non-goals for v1 - OAuth flows and app verification. - Credentials for other people. - Hundreds of connectors. - An HTTP transport reachable beyond your machine. - Hundreds of pre-built, pre-tested tool schemas you did not have to read API docs for - Managed OAuth: app registration, consent screens, refresh tokens, revocation - Per-end-user credential storage, which is the whole game if your agent serves anyone but you - Someone else fixing your integration when a provider ships a breaking API change - Usage observability, retries and rate limit smoothing across providers ## Success criteria - Every stdout line is confirmed to be a protocol message - A canary token is confirmed absent from logs and outputs - Retry-After is confirmed honoured to the second - A repeated idempotency key is confirmed to make no second call - The doctor command is confirmed to name the fix per provider
# Build brief · Composio The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local tool server that hands an agent your own accounts, and nobody else's to replace Composio. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Stack (fixed, do not substitute) - Protocol: The MCP SDK over stdio, with every log line on stderr · the specification reserves stdout for protocol messages - Language: TypeScript on Node 22 with a zod schema per tool · the schema is the contract the model reads - HTTP: One shared fetch helper with timeouts, Retry-After and a per-provider budget · each provider limits differently and the agent calls in loops - Credentials: Personal tokens from the environment or the platform keychain, never from tool arguments · an agent that can pass a token can leak one - Tests: A mocked-HTTP suite covering 401, 429 and repeated writes · the failure paths are the product ### Data model (create this before Phase 1) - providers(id, enabled, base_url, token_ref, scopes_declared, rate_policy_json) · what each token may do, written down - tools(name, provider_id, kind, input_schema, output_fields, description) · kind is read or write - calls(id, tool, at, duration_ms, status, provider_status, retry_after_s, bytes_returned) · shapes and statuses, never bodies - writes(idempotency_key, tool, at, provider_ref, result_json) · a write tool never repeats a key - budgets(provider_id, window_start, used, limit, unit) · from the provider's published limits, checked before the call - The calls log records what was asked and how the provider answered but never the content returned, because a log of everything the agent read is a second copy of your mailbox. ### Phase 1 · A server that speaks only protocol on stdout Build: Bootstrap the server with the SDK over stdio and a logger bound to stderr. At startup, check that every enabled provider has its token and exit non-zero naming the missing variable. Register one smoke tool and connect a real client. Trap: A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. Done when: The server lists its tools under a real MCP client. A test launches the server, drives a tool call and asserts every stdout line parses as a JSON-RPC message. A missing token for an enabled provider exits non-zero naming the variable, and a disabled provider's tools do not appear. Logging goes to stderr and is visible in the client's log. Do not build yet: No provider tools, budgets or write handling. ### Phase 2 · Read tools that return what the model needs Build: Read tools for your three or four providers: search messages, list issues, read a page, list channels. Each has a zod input, a one-line description, an output trimmed to named fields, a byte cap and a cursor for more. A 401 names the provider and the variable to fix, never the token. Trap: Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. Done when: Every read tool's output is under the stated byte cap, asserted in tests. Pagination works through cursors and a page fetched twice is identical. A 401 returns a message naming the provider and the environment variable without the token value, tested with a canary token that must not appear. Descriptions are one line and name the input the model must supply. Do not build yet: No budgets, retries or write tools yet. ### Phase 3 · Limits the agent cannot see, made visible Build: Per-provider budgets from the published limits, in a config file with the date you checked. Honour Retry-After on 429 exactly. Bound backoff and log it. When a call would exceed the budget, return a result saying how long to wait instead of blocking or retrying silently, and put the limit in the tool's description. Trap: For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. Done when: A 429 with Retry-After is honoured to the second, tested with a mock. Each provider has a budget in config with the date it was checked, and the tool description states it. Backoff is bounded and each retry is logged. Where a provider counts units rather than calls, such as Gmail's per-user per-minute quota in which a send costs a hundred units, the budget counts units. Do not build yet: No write tools or client packaging yet. ### Phase 4 · Write tools that cannot run twice Build: Write tools (post a message, create an issue, append a block) take a required idempotency key and a dry-run flag. The key and the result are stored before the provider is called and returned on a repeat. The writes table survives a restart. Trap: An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. Done when: Repeating a write with the same key returns the first result and makes no second provider call, tested with a mock that counts. A write without a key is rejected by the schema. Dry run returns exactly what would be sent. The writes table persists across a restart and a repeat after restart is still refused. Do not build yet: No doctor command or client packaging yet. ### Phase 5 · Handing it to a client, and where the tokens live Build: A doctor command that checks each provider's token, scopes and expiry and reports per provider. The exact configuration block for two MCP clients, referencing environment variables rather than pasting tokens. Scope documentation per provider, a scan proving no token appears in any log or output, and a README section on what is out. Trap: Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days. Done when: The doctor command reports each provider as connected, expired or missing with the fix named. The client configuration references variables and connects on a fresh machine following only the README. A canary token placed in every provider never appears in logs or tool results, scanned in a test. Expired tokens surface as a per-provider message rather than a generic failure, including a GitHub fine-grained token with the expiry you set. Do not build yet: Nothing after this. ### Out of scope (and why) - OAuth flows, consent screens and app verification, which are the part you pay a hosted layer for and which multiply per provider. - Credentials for anyone but you, since storing and refreshing other people's tokens is a security product rather than a weekend. - Hundreds of connectors, which is a maintenance treadmill; under five, written by hand, is the honest scope. - An HTTP transport reachable beyond your machine; if you expose one, the specification requires validating the Origin header and binding to localhost. ### README must contain - That stdout is the protocol channel and every log line goes to stderr, enforced by a test - That tokens come from the environment or the keychain, are never accepted as tool arguments, and never appear in output or logs - That each provider's limits are written in config with the date checked and stated in the tool description - That every write tool requires an idempotency key and a repeat returns the stored result - That Gmail has no personal token and needs an OAuth client whose refresh token expires weekly while the project is in Testing
# Architecture · Composio ## Stack | Part | Choice | Why | | --- | --- | --- | | Protocol | The MCP SDK over stdio, with every log line on stderr | the specification reserves stdout for protocol messages | | Language | TypeScript on Node 22 with a zod schema per tool | the schema is the contract the model reads | | HTTP | One shared fetch helper with timeouts, Retry-After and a per-provider budget | each provider limits differently and the agent calls in loops | | Credentials | Personal tokens from the environment or the platform keychain, never from tool arguments | an agent that can pass a token can leak one | | Tests | A mocked-HTTP suite covering 401, 429 and repeated writes | the failure paths are the product | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Transport | The stdio server and the stderr logger. | Any transport, provided nothing but protocol reaches the channel. | | Providers | Hand-written read and write tools per service. | Any service, provided outputs are trimmed and capped. | | Budgets | Dated limits and Retry-After handling. | Any policy, provided the wait is returned rather than slept through. | | Writes | Idempotency keys stored before the call. | Not swappable for retry-and-hope, which posts twice. | | Doctor | Per-provider token, scope and expiry checks. | Any implementation, provided it names the fix. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `PROVIDERS` · required · Enabled providers. A missing token for any of these stops startup. - `GITHUB_TOKEN` · optional, secret · Fine-grained, least privilege, with an expiry. The doctor command reports when it lapses. - `SLACK_USER_TOKEN` · optional, secret · Your own workspace. History calls are budgeted at the published non-Marketplace rate. - `NOTION_TOKEN` · optional, secret · An integration shared with only the pages the tools should see. - `LIMITS_PATH` · required · Per-provider budgets with the date each was checked. - `MAX_RESULT_BYTES` · required · Every read tool trims to this. The model's context is the scarce resource. - `WRITES_DB_PATH` · required · Idempotency keys and results. Must survive a restart. - `LOG_LEVEL` · required · Logs go to stderr only. There is no level that writes to stdout. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Composio product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: The MCP SDK over stdio, with every log line on stderr, TypeScript on Node 22 with a zod schema per tool, One shared fetch helper with timeouts, Retry-After and a per-provider budget, Personal tokens from the environment or the platform keychain, never from tool arguments, A mocked-HTTP suite covering 401, 429 and repeated writes. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. - Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days.
# Delivery milestones · Composio Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · A server that speaks only protocol on stdout The server runs under a real client and nothing but MCP messages ever reaches stdout. ### Steps 1. Bind logging to stderr and guard stdout One stray print breaks the protocol; make it impossible rather than unlikely. Files: `src/index.ts`, `src/log.ts`, `tests/stdout-clean.test.ts` 2. Fail at startup on a missing token Naming the variable, before any client connects. Files: `src/config.ts` 3. Connect a real client The exact JSON block for two clients belongs in the README from day one. Files: `README.md` ### Done when - [ ] Every stdout line is a valid protocol message - [ ] A missing token exits non-zero naming the variable - [ ] Disabled providers show no tools - [ ] Logs reach stderr and the client's log ### Watch out - A dependency that prints a deprecation notice to stdout, or one console.log left in a handler, corrupts the message stream and the client reports a parse error with no hint of the cause. Redirect all non-transport stdout writes to stderr at startup, and scan the stream in a test for anything that is not a newline-delimited JSON message. ## M2 · Read tools that return what the model needs Each read tool returns trimmed, bounded, paginated results with a description the model can act on. ### Steps 1. Trim outputs to named fields The model's context is the scarce resource. Files: `src/providers/github.ts`, `src/providers/slack.ts`, `src/providers/notion.ts` 2. Paginate with cursors and cap bytes A bounded page beats a truncated dump. Files: `src/paging.ts`, `tests/output-size.test.ts` 3. Make a 401 name the fix, not the secret Test with a canary token that must never appear. Files: `src/http.ts`, `tests/no-token-leak.test.ts` ### Done when - [ ] Outputs stay under the byte cap - [ ] Cursors page correctly - [ ] A 401 names provider and variable, never the token - [ ] Descriptions are one actionable line ### Watch out - Returning the provider's raw response hands the model forty kilobytes of metadata to find one subject line, and after three calls the conversation is out of context. Trim to the fields a person would read, cap the count, and return a cursor. ## M3 · Limits the agent cannot see, made visible Provider rate limits are known before the call and returned as a wait, never discovered as a ban. ### Steps 1. Write the limits down with a date They change; the config says when you last looked. Files: `config/limits.json`, `src/budgets.ts` 2. Honour Retry-After exactly To the second. A guess is a longer wait. Files: `src/http.ts`, `tests/retry-after.test.ts` 3. Return the wait rather than retrying The agent can plan around a number; it cannot see a silent sleep. Files: `src/budgets.ts` ### Done when - [ ] Retry-After is honoured to the second - [ ] Every provider has a dated budget - [ ] Backoff is bounded and logged - [ ] Unit-based quotas count units ### Watch out - For Slack apps outside its Marketplace, message history calls are limited to one request per minute with at most fifteen results, so an agent paging through a channel hits the limit on its second call and, if the server retries for it, retries into a longer wait. Put the limit in the description, return the wait, and never retry a 429 more than once. ## M4 · Write tools that cannot run twice Sending, posting and creating happen once per intent, however many times the agent asks. ### Steps 1. Require the key in the schema The model cannot omit what the schema demands. Files: `src/writes.ts`, `src/providers/slack.ts` 2. Store before calling, return on repeat Tested with a provider mock that counts calls. Files: `src/writes.ts`, `tests/idempotent-writes.test.ts` 3. Offer dry run on every write What would be sent, verbatim, with no side effect. Files: `src/writes.ts` ### Done when - [ ] A repeated key makes no second call - [ ] Keyless writes are rejected - [ ] Dry run shows exactly what would be sent - [ ] Keys survive a restart ### Watch out - An agent whose tool call timed out will call again with the same arguments, and without a key you have posted twice to a channel or opened two issues before anyone notices. Require the key in the schema, store it before calling, and return the stored result on a repeat without touching the provider. ## M5 · Handing it to a client, and where the tokens live (production only) Anyone with the README can connect a client, and a doctor command says what is wrong before the agent does. ### Steps 1. Write the doctor command Connected, expired or missing, per provider, with the fix. Files: `src/doctor.ts` 2. Reference tokens, never paste them In the client config and in the README. Files: `README.md`, `config/clients/` 3. Scan for the canary A token that appears anywhere fails the build. Files: `tests/canary-scan.test.ts` ### Done when - [ ] Doctor reports per provider with the fix - [ ] A fresh machine connects from the README - [ ] The canary appears nowhere - [ ] Expiry surfaces per provider ### Watch out - Pasting tokens into the client's configuration file means every screenshot, backup and support thread that includes it includes them. Reference the environment or the keychain from the config, and for a provider such as Gmail that has no personal token at all, say so: it needs an OAuth client, and one left in Google's Testing status issues refresh tokens that expire after seven days.
# Operations · Composio ## Backup The writes database is the only state that matters, since losing it means a repeated key is no longer recognised. Back it up with the machine; everything else is regenerated from config and tokens. ## Restore Restore and confirm the writes table is present before starting the server, since a server started against an empty table will happily repeat every write the agent retries. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Budget consumption per provider and the count of 429s. A 429 that reaches the log means a budget is wrong or the provider changed its limit, and the config's date tells you which is more likely. ## Incident checklist A token in a log or a tool result is the incident: revoke it at the provider, rotate, and add the leaked pattern to the canary scan. A duplicated write is the second kind, and it means a write path bypassed the key store. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Every stdout line is confirmed to be a protocol message - [ ] A canary token is confirmed absent from logs and outputs - [ ] Retry-After is confirmed honoured to the second - [ ] A repeated idempotency key is confirmed to make no second call - [ ] The doctor command is confirmed to name the fix per provider ## Launch constraint Do not market omitted Composio capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Enabled providers. A missing token for any of these stops startup. PROVIDERS=github,slack,notion # Optional · secret. Fine-grained, least privilege, with an expiry. The doctor command reports when it lapses. GITHUB_TOKEN=github_pat_...redacted # Optional · secret. Your own workspace. History calls are budgeted at the published non-Marketplace rate. SLACK_USER_TOKEN=xoxp-...redacted # Optional · secret. An integration shared with only the pages the tools should see. NOTION_TOKEN=ntn_...redacted # Required. Per-provider budgets with the date each was checked. LIMITS_PATH=./config/limits.json # Required. Every read tool trims to this. The model's context is the scarce resource. MAX_RESULT_BYTES=16000 # Required. Idempotency keys and results. Must survive a restart. WRITES_DB_PATH=./data/writes.sqlite # Required. Logs go to stderr only. There is no level that writes to stdout. LOG_LEVEL=info
$ choose a build depth, inspect the files, then open the complete pack in your agent
Because integrations are boring, endless maintenance work with a long tail nobody can pre-plan. Writing one tool wrapper is trivial; writing eighty and keeping them alive through provider changes is a team. The real unlock is managed OAuth for other people's accounts: the second your agent has users, you are storing and refreshing third-party credentials, and that is a security and support problem you can rent your way out of.
xHundreds of pre-built, pre-tested tool schemas you did not have to read API docs for
xManaged OAuth: app registration, consent screens, refresh tokens, revocation
xPer-end-user credential storage, which is the whole game if your agent serves anyone but you
xSomeone else fixing your integration when a provider ships a breaking API change
xUsage observability, retries and rate limit smoothing across providers
Nothing worth pointing at. That's why the prompt exists.
Vibecode Composio
Kinda. The core of Composio is buildable in a weekend with the prompt on this page, but there are real gaps: Hundreds of pre-built, pre-tested tool schemas you did not have to read API docs for, Managed OAuth: app registration, consent screens, refresh tokens, revocation. Read the honest list above before committing.
How much does Composio cost?
Composio costs about $29/month (Pro, checked 2026-08-18), which is $348 per year.
What do I lose by replacing Composio?
Honestly: Hundreds of pre-built, pre-tested tool schemas you did not have to read API docs for; Managed OAuth: app registration, consent screens, refresh tokens, revocation; Per-end-user credential storage, which is the whole game if your agent serves anyone but you; Someone else fixing your integration when a provider ships a breaking API change; Usage observability, retries and rate limit smoothing across providers. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Composio?
No mature open-source alternative worth pointing at, which is exactly why the one-shot prompt on this page exists.