Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ export default defineConfig({
{ label: "Linear", link: "/integrations/linear/" },
{ label: "Notion", link: "/integrations/notion/" },
{ label: "Sentry", link: "/integrations/sentry/" },
{ label: "Twenty CRM", link: "/integrations/twenty/" },
{ label: "Chatwoot", link: "/integrations/chatwoot/" },
{ label: "Web access", link: "/integrations/web-tools/" },
{ label: "Research in your Chrome", link: "/integrations/chrome/" },
{ label: "Research on Reddit", link: "/integrations/reddit/" },
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/inference/models-json.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ A top-level `mcpServers` block (a sibling of `active`/`models`) configures [MCP
}
```

A project's own `tsforge.config.json` `mcpServers` entry with the same name overrides this one entirely; entries unique to either side apply together. See [MCP servers](/integrations/mcp/) for the field reference and the curated Linear/Notion/Sentry integrations.
A project's own `tsforge.config.json` `mcpServers` entry with the same name overrides this one entirely; entries unique to either side apply together. See [MCP servers](/integrations/mcp/) for the field reference (including `"type": "http"` servers with auth `headers`, like [Twenty](/integrations/twenty/)) and the curated Linear/Notion/Sentry/Twenty integrations.

### Any other provider: `extraBody` / `extraHeaders`

Expand Down
78 changes: 78 additions & 0 deletions apps/docs/src/content/docs/integrations/chatwoot.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
title: Chatwoot
description: "Let the agent work your Chatwoot support inbox: read conversations, draft replies as private notes, send when you say so, and triage with status, assignment and labels."
---

[Chatwoot](https://www.chatwoot.com) is an open-source customer support inbox you can host yourself. With it connected, tsforge can read what customers wrote, look people up, draft or send replies, and keep the inbox tidy.

Chatwoot has no MCP server, so tsforge talks to its REST API directly with your access token. It follows the same model as the [MCP integrations](/integrations/mcp/): off until configured, and a small set of purpose-built verbs.

## Turn it on

1. In Chatwoot, click your avatar (bottom left), open **Profile Settings**, and copy the **Access Token** at the bottom of the page.
2. Put it in `~/.tsforge/config.json` with your instance URL and account id. The account id is the number in the address bar: `…/app/accounts/<id>/…`.

```json title="~/.tsforge/config.json"
{
"settings": {
"chatwootUrl": "https://support.example.com",
"chatwootAccountId": 1,
"chatwootToken": "<your access token>"
}
}
```

With all three set, an interactive session shows `chatwoot: on · support inbox via REST`. The agent acts as the user who owns the token, so replies go out under that agent's name.

## The tools

**`chatwoot_read`** (read-only):

| op | What it returns |
| --- | --- |
| `conversations` | the inbox. `status` is `open` (default), `pending`, `resolved`, `snoozed` or `all`; `assignee` is `me`, `unassigned` or `all`; plus `inbox` and `page`. Each row shows the customer, assignee, unread count, labels and the last message |
| `conversation` | one conversation `id` (the number in `#123`) with its messages in order: customer, agent, private notes and activity |
| `contacts` | people matching `query` (name, email or phone) |
| `contact` | one contact `id` with their attributes and conversations |
| `inboxes`, `agents`, `labels` | lookups for filtering and assigning |

**`chatwoot_write`** on a conversation `id`:

| op | What it does |
| --- | --- |
| `reply` | sends `body` **to the customer, immediately** |
| `note` | adds a **private** note (`body`) that only agents see |
| `status` | sets `open`, `pending`, `resolved` or `snoozed` |
| `assign` | assigns to `"me"`, or an agent by id, name or email (a name that matches more than one agent is refused) |
| `label` | adds `labels`; existing labels are kept |

## Replies reach real people

A reply can't be unsent, so the agent is told to **send one only when you asked it to reply**. Otherwise it writes its draft as a private note for a human to review and send. Writes are also held back while planning or running unattended, like every integration write. See [Permissions & policy](/guardrails/policy/).

**Customer messages are untrusted input.** Conversation text reaches the model marked as data, and the agent is told never to follow instructions inside a message or paste internal notes, secrets or other customers' details into a reply.

For safety, tsforge never follows an HTTP redirect with your token. A token that stops working comes back as `HTTP 401: Chatwoot rejected the access token`.

## With Twenty

With [Twenty](/integrations/twenty/) connected too, the agent can look the customer up in the CRM while it reads their conversation, and log the outcome as a note on their person record.

| Setting / variable | Effect |
| --- | --- |
| `chatwootUrl` / `TSFORGE_CHATWOOT_URL` | your Chatwoot instance |
| `chatwootToken` / `TSFORGE_CHATWOOT_TOKEN` | your access token |
| `chatwootAccountId` / `TSFORGE_CHATWOOT_ACCOUNT_ID` | the account to work in |
| `TSFORGE_NO_CHATWOOT` | withhold the Chatwoot tools even when configured (`=1`) |

## Checking it against your instance

```bash
TSFORGE_LIVE=1 bun test packages/core/tests/integrations-live.test.ts
# also add a private note + a label, and toggle status and back, on conversation 42:
TSFORGE_LIVE=1 TSFORGE_LIVE_CHATWOOT_CONVERSATION=42 bun test packages/core/tests/integrations-live.test.ts
```

The write run never sends a reply. It restores the conversation's status and labels afterwards; the private note stays, marked as a test.

→ [Twenty CRM](/integrations/twenty/) · [MCP servers](/integrations/mcp/) · [Environment variables](/reference/flags/)
2 changes: 1 addition & 1 deletion apps/docs/src/content/docs/integrations/linear.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ The server **must be keyed exactly `linear`**. Once it connects, an interactive
## The tools

- **`linear_read`** (read-only): `issue` (one card by identifier like `ENG-123` — title, state, description, the **branch name** Linear generated, and links), `search`, `mine` (your assigned issues), and `comments`.
- **`linear_write`**: `create` (a new card — returns its identifier and branch name) and `comment`. There is deliberately **no status op**: Linear moves the card itself when the linked PR opens and merges.
- **`linear_write`**: `create` (a new card with a `title` and the `team` to file it under, by key like `ENG`, name or id; returns its identifier and branch name) and `comment` (adds a comment to a card by its identifier). There is deliberately **no status op**: Linear moves the card itself when the linked PR opens and merges.
- **`linear_start`**: the one-step start — read a card by id and **check out the git branch** Linear made for it (creating it if needed). Edit as usual, then open a PR referencing the issue (e.g. `Fixes ENG-123`); Linear links the branch and transitions the card for you.

## Cards written for humans
Expand Down
36 changes: 28 additions & 8 deletions apps/docs/src/content/docs/integrations/mcp.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ title: MCP servers
description: Give the agent extra tools from external Model Context Protocol servers. Live library docs, search, and more.
---

tsforge can connect to external **MCP (Model Context Protocol) servers** and offer their tools to the agent during an interactive session. The classic use is **[Context7](https://github.com/upstash/context7)** for up-to-date library documentation, but any stdio MCP server works.
tsforge can connect to external **MCP (Model Context Protocol) servers** and offer their tools to the agent during an interactive session. The classic use is **[Context7](https://github.com/upstash/context7)** for up-to-date library documentation, but any MCP server works, spawned locally over stdio or reached over HTTP.

MCP is **opt-in**: with no `mcpServers` block in your config, nothing connects and nothing changes.

## Configure

Add an `mcpServers` block to `tsforge.config.json` at your repo root. Each entry is a stdio server tsforge spawns. `${VAR}` references are read from the environment at startup.
Add an `mcpServers` block to `tsforge.config.json` at your repo root. An entry is either a stdio server tsforge spawns, or (`"type": "http"`) a server it reaches at a URL. `${VAR}` references are read from the environment at startup.

```json
{
Expand All @@ -28,7 +28,9 @@ Add an `mcpServers` block to `tsforge.config.json` at your repo root. Each entry
| `command` | yes (stdio) | Executable to spawn |
| `args` | no | Arguments for `command` |
| `env` | no | Extra env vars (merged over the parent env); `${VAR}` interpolated |
| `type` | no | `stdio` (default) or `http` (http not yet wired) |
| `type` | no | `stdio` (default) or `http` |
| `url` | yes (http) | The server's MCP endpoint, e.g. `https://crm.example.com/mcp` |
| `headers` | no (http) | Headers sent on every request, e.g. `{ "Authorization": "Bearer …" }`; `${VAR}` interpolated |
| `timeoutMs` | no | Per-call timeout (default 30000) |

### Global servers (`~/.tsforge/models.json`)
Expand All @@ -50,17 +52,35 @@ An `mcpServers` block also works in the global model registry, `~/.tsforge/model

On startup tsforge connects each server, lists its tools, and advertises them to the model under a namespaced name: `mcp__<server>__<tool>` (for example `mcp__context7__get-library-docs`). When the model calls one, tsforge routes it to that server and feeds the text result back into the conversation.

## Built-in integrations (Linear, Notion, Sentry)
## Built-in integrations (Linear, Notion, Sentry, Twenty)

Some MCP servers get a **curated** treatment instead of raw passthrough. When you configure a server keyed **`linear`**, **`notion`**, or **`sentry`**, tsforge offers a small set of purpose-built verbs (e.g. `linear_read`, `notion_read`, `sentry_read`) instead of that server's dozens of raw tools — so the model's tool list stays focused — and hides the raw `mcp__<server>__*` tools by default (re-expose them with the matching `TSFORGE_<NAME>_RAW=1`). These follow a **capability = consent** model: the server being connected is your consent, reads work in every mode, and writes are held back while planning or running unattended.
Some MCP servers get a **curated** treatment instead of raw passthrough. When you configure a server keyed **`linear`**, **`notion`**, **`sentry`** or **`twenty`**, tsforge offers a small set of purpose-built verbs (e.g. `linear_read`, `notion_read`, `sentry_read`) instead of that server's dozens of raw tools — so the model's tool list stays focused — and hides the raw `mcp__<server>__*` tools by default (re-expose them with the matching `TSFORGE_<NAME>_RAW=1`). These follow a **capability = consent** model: the server being connected is your consent, reads work in every mode, and writes are held back while planning or running unattended.

[GitHub](/integrations/git-github/) is first-class too, but via the `git`/`gh` binaries rather than MCP.

Full guides: [Git & GitHub](/integrations/git-github/) · [Linear](/integrations/linear/) · [Notion](/integrations/notion/) · [Sentry](/integrations/sentry/).
[Chatwoot](/integrations/chatwoot/) works the same way but over its REST API, since it has no MCP server.

Full guides: [Git & GitHub](/integrations/git-github/) · [Linear](/integrations/linear/) · [Notion](/integrations/notion/) · [Sentry](/integrations/sentry/) · [Twenty CRM](/integrations/twenty/) · [Chatwoot](/integrations/chatwoot/).

### Remote (hosted) servers

tsforge's MCP transport is **stdio**. To reach a hosted server that speaks HTTP/SSE (like Linear's, Notion's, or Sentry's), bridge it with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) as the stdio command:
A server that speaks **Streamable HTTP** and takes a token in a header connects directly with `"type": "http"`. Self-hosted apps such as [Twenty](/integrations/twenty/) work this way:

```json
{
"mcpServers": {
"twenty": {
"type": "http",
"url": "https://crm.example.com/mcp",
"headers": { "Authorization": "Bearer <your Twenty API key>" }
}
}
}
```

tsforge sends your headers on every request, keeps the session the server issues (and starts a fresh one if the server forgets it), accepts JSON or streamed replies, and reports a rejected key as `HTTP 401 (unauthorized …)` at startup.

Hosted servers that sign you in through a browser (OAuth), like Linear's, Notion's or Sentry's, still go through [`mcp-remote`](https://www.npmjs.com/package/mcp-remote), which handles the sign-in:

```json
{
Expand All @@ -80,7 +100,7 @@ MCP fits tsforge's deterministic, local-first design as an **opt-in context sour

## Limits today

- **stdio transport only**: `http` entries are reported and skipped (HTTP/SSE is planned).
- The `http` transport takes a token in a header. It does not run a browser sign-in (OAuth); use `mcp-remote` for servers that need one.
- MCP tools are offered in the **interactive CLI**; the headless eval path does not load them.
- Connected servers are child processes; they exit with the tsforge process.

Expand Down
83 changes: 83 additions & 0 deletions apps/docs/src/content/docs/integrations/twenty.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
title: Twenty CRM
description: "Let the agent work your Twenty CRM: find people, companies and deals, log notes and follow-up tasks against them, and read the pipeline."
---

[Twenty](https://twenty.com) is an open-source CRM you can host yourself. With it connected, tsforge can look up a customer before replying to them, log what happened on a call or a support conversation, add a follow-up task, and tell you how the pipeline looks.

Twenty is a **curated integration over MCP**. See [MCP servers](/integrations/mcp/) for the shared model: the connection is your consent, the agent gets a few purpose-built verbs, and nothing turns on until you configure it.

## Turn it on

1. In Twenty, open **Settings → APIs & Webhooks → API** and create an API key.
2. Add a `twenty` server to `mcpServers` in `~/.tsforge/models.json` (or a project's `tsforge.config.json`). Twenty's MCP endpoint is plain HTTP, so it uses tsforge's built-in [HTTP transport](/integrations/mcp/#remote-hosted-servers). You don't need `mcp-remote`.

```json title="~/.tsforge/models.json"
{
"mcpServers": {
"twenty": {
"type": "http",
"url": "https://crm.example.com/mcp",
"headers": { "Authorization": "Bearer <your Twenty API key>" }
}
}
}
```

The server **must be keyed exactly `twenty`**. Twenty's **Settings → MCP & APIs** page shows this same block with your instance's URL filled in. When it connects, an interactive session shows `twenty: on · CRM via MCP`. A wrong key shows up at startup as `HTTP 401 (unauthorized …)`.

## The tools

Twenty's MCP server offers about 300 generated tools behind a catalog the model would have to browse, learn and call in three steps. tsforge wraps the ones a CRM workflow needs in two verbs and sends Twenty exactly the argument shapes it validates. The model never sees the catalog.

**`twenty_read`** (read-only). `type` is `person`, `company`, `opportunity`, `task` or `note`.

| op | What it returns |
| --- | --- |
| `search` | records matching `query`: names and emails for people, name or domain for companies, titles for deals, tasks and notes |
| `list` | the most recently updated records (`limit`, default 10, max 50) |
| `record` | one record by `id`, with the notes and tasks attached to it |
| `pipeline` | opportunities counted and summed by stage |

Every row ends with the record's id in parentheses. That id is what `record`, `update`, `note` and `task` take.

**`twenty_write`**:

| op | What it does |
| --- | --- |
| `create` | a person (`firstName`, `lastName`, `email`, `phone`, `jobTitle`, `companyId`), a company (`name`, `domain`) or an opportunity (`name`, `stage`, `amount` + `currency`, `closeDate`, `companyId`, `pointOfContactId`) |
| `update` | `type` + `id` + only the fields to change |
| `note` | `title` + `body` (markdown); add `type` + `id` to attach it to that person, company or deal |
| `task` | `title`, optional `body`, `dueAt`, `status` (`TODO`, `IN_PROGRESS`, `DONE`); `type` + `id` to attach it |

Amounts are whole currency units (`5000` means 5,000). tsforge converts them to the micros Twenty stores. A new deal starts at stage `NEW` unless you pass one.

There is **no delete**. If a record needs to go, the agent tells you.

## Things worth knowing

- **Search before create.** The agent is told to look a person or company up before adding one. Twenty enforces unique emails and company domains, and **records in Twenty's trash still count** until the trash is emptied. A duplicate comes back as a plain "that record already exists" instead of a database error.
- **Notes are how the agent leaves a trail.** A support conversation in [Chatwoot](/integrations/chatwoot/) can end with a note on the customer's person record, so the next person to open them sees what happened.

## When it's active

Reads work in every mode, including [plan mode](/cli/plan-mode/). Writes follow capability as consent: allowed in an interactive session, held back while planning or running unattended. See [Permissions & policy](/guardrails/policy/).

| Variable | Default | Effect |
| --- | --- | --- |
| `TSFORGE_NO_TWENTY` | off | withhold the Twenty tools even when the server is connected (`=1`) |
| `TSFORGE_TWENTY_RAW` | off | also advertise Twenty's raw `mcp__twenty__*` meta-tools (`=1`) |

## Checking it against your instance

tsforge's tests check every call against Twenty's real input schemas (from v2.41). A live suite runs the verbs against your own instance:

```bash
TSFORGE_LIVE=1 bun test packages/core/tests/integrations-live.test.ts
# also create → update → note → task → read back → delete test records:
TSFORGE_LIVE=1 TSFORGE_LIVE_WRITES=1 bun test packages/core/tests/integrations-live.test.ts
```

The write run uses records named `tsforge-test-…` and removes them for good afterwards, trash included.

→ [MCP servers](/integrations/mcp/) · [Chatwoot](/integrations/chatwoot/) · [Environment variables](/reference/flags/)
Loading
Loading