Metadata-Version: 2.5
Name: withfeedback-mcp
Version: 0.1.0
Summary: MCP server for withfeedback.com — testimonials, feedback moderation, widgets, and NPS for AI agents
Project-URL: Homepage, https://withfeedback.com
Project-URL: Documentation, https://withfeedback.com/api/docs/
License: MIT
Keywords: feedback,mcp,testimonials,withfeedback
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: mcp>=1.2
Description-Content-Type: text/markdown

# withfeedback-mcp

MCP (Model Context Protocol) server for [withfeedback.com](https://withfeedback.com):
lets AI assistants (Claude, Cursor, …) list and moderate feedback submissions,
create and bulk-import testimonials, inspect widgets and embed codes, and read
NPS survey results.

Transports: **stdio** (default) and **streamable HTTP** (`withfeedback-mcp --http`
or `WITHFEEDBACK_MCP_TRANSPORT=http`). HTTP mode binds `127.0.0.1` only —
see the security note below before changing that.

## Install

```bash
pip install withfeedback-mcp
# or run without installing:
uvx withfeedback-mcp
```

## Quick start with a personal access token (PAT)

The recommended setup for a local/stdio MCP server:

1. Log in at your withfeedback.com instance and open **Account → API Tokens**
   (`/accounts/tokens/`).
2. Create a token with the scopes you need (see the table below). Copy it —
   it is shown only once.
3. Configure your MCP client with `WITHFEEDBACK_TOKEN` (examples below).

API access requires a plan that includes the API/CLI/MCP feature (Pro and
above). Requests from lower plans return HTTP 403.

## Claude Desktop / Claude Code configuration

Claude Desktop (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "withfeedback": {
      "command": "uvx",
      "args": ["withfeedback-mcp"],
      "env": {
        "WITHFEEDBACK_TOKEN": "spd_your_token_here"
      }
    }
  }
}
```

Prefer the OAuth2 device flow over a PAT? Omit `WITHFEEDBACK_TOKEN` — the
official public client id `7yQMsnY2Is2f5tCuwwgItoQu3fRkEX2wnzIRj0Vh` is the
built-in default (shown here explicitly; only needed to override):

```json
{
  "mcpServers": {
    "withfeedback": {
      "command": "uvx",
      "args": ["withfeedback-mcp"],
      "env": {
        "WITHFEEDBACK_CLIENT_ID": "7yQMsnY2Is2f5tCuwwgItoQu3fRkEX2wnzIRj0Vh"
      }
    }
  }
}
```

Claude Code:

```bash
claude mcp add withfeedback --env WITHFEEDBACK_TOKEN=spd_your_token_here -- uvx withfeedback-mcp
```

## Cursor configuration

`~/.cursor/mcp.json` (or the project's `.cursor/mcp.json`):

```json
{
  "mcpServers": {
    "withfeedback": {
      "command": "uvx",
      "args": ["withfeedback-mcp"],
      "env": {
        "WITHFEEDBACK_TOKEN": "spd_your_token_here"
      }
    }
  }
}
```

## OAuth2 device flow (alternative to a PAT)

If `WITHFEEDBACK_TOKEN` is not set, the server falls back to the OAuth2
device flow using the **official public client id**
`7yQMsnY2Is2f5tCuwwgItoQu3fRkEX2wnzIRj0Vh` (public by design — device-flow
clients carry no secret). Set `WITHFEEDBACK_CLIENT_ID` to use a different
client (e.g. on a self-hosted instance). On the first tool call the server
prints a verification URL and a short code on stderr (and tries to open your
browser):

1. The server calls `POST /o/device-authorization/` with the client id.
2. You open the printed URL, enter the code, and approve the requested scopes.
3. The server polls `POST /o/token/` until approval and then uses the access
   token for all API calls.

Request a custom scope set with `WITHFEEDBACK_SCOPES` (space-separated).

**Note on Dynamic Client Registration:** open DCR (`POST /o/register/`) is
**disabled in production** (`DCR_ENABLED=False` is the production default) —
the endpoint is unauthenticated and unthrottled, so withfeedback.com does not
expose it. Use the published official client id (or a PAT); self-hosted
instances can register additional clients via Django admin or the
`create_oauth2_app` management command.

## Environment variables

| Variable | Default | Purpose |
|---|---|---|
| `WITHFEEDBACK_API_URL` | `https://withfeedback.com` | API base URL (self-hosted/staging override) |
| `WITHFEEDBACK_TOKEN` | — | Personal access token (primary auth for stdio) |
| `WITHFEEDBACK_CLIENT_ID` | official public client id | OAuth2 client id for the device-flow fallback |
| `WITHFEEDBACK_SCOPES` | all tool scopes | Scopes requested during device flow |
| `WITHFEEDBACK_MCP_TRANSPORT` | `stdio` | Set to `http` for streamable HTTP |
| `WITHFEEDBACK_MCP_HOST` | `127.0.0.1` | HTTP bind address (loopback only by default) |
| `WITHFEEDBACK_MCP_PORT` | `8000` | HTTP bind port |
| `WITHFEEDBACK_MCP_ALLOW_REMOTE` | unset | Must be `1` to allow a non-loopback HTTP bind (read the security note first) |

On startup the server checks `GET /.well-known/speedpy.json` once and warns
(never fails) when the manifest is unreachable or the API is older than the
minimum this client expects.

## HTTP transport security

The streamable HTTP transport has **no per-caller authentication**: the
process holds a single account token (your PAT or device-flow grant), and
every HTTP caller acts as that account. Therefore:

- HTTP mode binds **`127.0.0.1` by default** and is intended for local
  clients on the same machine.
- Binding a non-loopback address is **refused** unless you explicitly set
  `WITHFEEDBACK_MCP_ALLOW_REMOTE=1`, and even then the server prints a
  prominent warning: only do this behind a reverse proxy that authenticates
  callers before they can reach the port.
- A properly multi-tenant **remote MCP server with per-user OAuth is a
  future server-side deployment of withfeedback.com** — it is not something
  this package provides. Do not expose this process to the internet.

## Tools and required scopes

Give your PAT (or device-flow grant) the scopes for the tools you plan to use:

| Tool | Endpoint(s) | Required scope |
|---|---|---|
| `list_teams` | `GET /api/v1/teams/` | `read:teams` |
| `list_projects` | `GET /api/v1/teams/{team}/projects/` | `read:feedback` |
| `list_submissions` | `GET …/projects/{id}/submissions/` | `read:feedback` |
| `get_submission` | `GET …/submissions/{id}/` | `read:feedback` |
| `approve_submission` | `POST …/submissions/{id}/approve/` | `moderate:submissions` |
| `reject_submission` | `POST …/submissions/{id}/reject/` | `moderate:submissions` |
| `create_testimonial` | `POST …/projects/{id}/submissions/` | `write:testimonials` |
| `import_testimonials` | `POST …/projects/{id}/submissions/` (looped, ≤100 items) | `write:testimonials` |
| `list_widgets` | `GET …/projects/{id}/widgets/` | `read:widgets` |
| `get_widget_embed_code` | `GET …/widgets/{id}/embed-code/` | `read:widgets` |
| `get_nps_summary` | `GET …/surveys/` + `GET …/surveys/{id}/results/` | `read:surveys` |

Notes:

- **Moderation is explicit and audited.** `approve_submission` /
  `reject_submission` call the dedicated action endpoints; `write:*` scopes
  cannot approve — only `moderate:submissions` can. Responses include the
  submission `id`, `status`, `moderated_by_id`, and `moderated_at`, so agent
  actions stay attributable in the moderation audit trail.
- **Nothing is auto-published.** `create_testimonial` and
  `import_testimonials` always create *pending* submissions that a moderation
  step (human or explicit approve call) must publish.
- API errors surface as readable tool errors: HTTP 402 = plan quota/billing
  block, HTTP 403 = missing scope/role/plan feature, HTTP 409 = invalid
  moderation state transition.

## Development

```bash
uv run --with pytest --with httpx --with mcp pytest packages/withfeedback-mcp/
```
