# ParrotNotes MCP Server

Connect your ParrotNotes account to Claude, ChatGPT, or any MCP client, and work with your meeting notes from inside the chat.

| | |
|---|---|
| **Endpoint** | `https://mcp.parrotnotes.app/mcp` |
| **Transport** | Streamable HTTP |
| **Authentication** | OAuth 2.0 with Dynamic Client Registration (RFC 7591) |
| **Scopes** | `openid` `email` `profile` |
| **Tools** | 9 (8 read-only, 1 write) |
| **Status** | Generally available |

## What it does

ParrotNotes records in-person conversations on your phone or Mac and turns them into transcripts, summaries and action items. This server exposes that library to an AI assistant so you can ask questions of your own notes without opening the app.

Typical requests:

- "Show me my five most recent recordings."
- "Find my notes from the Henderson site visit and summarise the key points."
- "What did this client commit to? Save that back as an action list."
- "List everything tagged project-alpha."

## Before you start

- A ParrotNotes account. The free tier works.
- At least one saved note or recording, otherwise every tool returns an empty result.
- Notes are created in the ParrotNotes apps for iOS, Android and macOS. This server reads that existing library and can save an insight back to a note. It cannot create recordings.

No API key, client ID or manual configuration is required. Clients register themselves through Dynamic Client Registration and you sign in through the browser.

## Connecting

### Claude

1. Open **Settings > Connectors > Add custom connector**.
2. Paste the endpoint:

```text
https://mcp.parrotnotes.app/mcp
```

3. You are redirected to `https://parrotnotes.app/auth/authorize` to sign in and grant consent.
4. After approval the tools are available in any conversation.

### Claude Code

```bash
claude mcp add --transport http parrotnotes https://mcp.parrotnotes.app/mcp
```

Then run `/mcp` inside Claude Code, select `parrotnotes` and complete the sign-in in your browser.

### ChatGPT

Add the same endpoint as a custom connector. The OAuth flow is identical.

### MCP Inspector

```bash
npx @modelcontextprotocol/inspector
```

Set transport to **Streamable HTTP**, URL to `https://mcp.parrotnotes.app/mcp`, and connect. The inspector completes the OAuth flow in the browser.

### Verify discovery

An unauthenticated request returns `401` with a pointer to the protected resource metadata, which is how clients discover the authorization server:

```bash
curl -i -X POST https://mcp.parrotnotes.app/mcp \
  -H "Content-Type: application/json" \
  -d '{}'
```

```http
HTTP/2 401
www-authenticate: Bearer realm="parrot-notes", resource_metadata="https://mcp.parrotnotes.app/.well-known/oauth-protected-resource", scope="openid email profile"
```

```bash
curl https://mcp.parrotnotes.app/.well-known/oauth-protected-resource
```

The response lists the resource (`https://mcp.parrotnotes.app/mcp`), its `authorization_servers`, `scopes_supported` and `bearer_methods_supported` (`header`).

## Tools

| Tool | What it does | Access |
|---|---|---|
| `get_recent_notes` | Lists recent notes and recordings, with pagination and a type filter | Read |
| `search_notes` | Full-text search across your notes by keyword | Read |
| `get_note` | Returns a single note by ID with its full transcript and content | Read |
| `get_note_insights` | Returns AI insights saved against a note: summaries, action items, sentiment | Read |
| `get_user_labels` | Lists every label you have created | Read |
| `get_note_labels` | Returns the labels assigned to a specific note | Read |
| `get_notes_by_label` | Returns all notes carrying a given label | Read |
| `get_subscription_status` | Returns your current subscription status | Read |
| `save_note_insight` | Saves an AI-generated insight against a note | **Write** |

Eight of the nine tools are read-only and carry `readOnlyHint: true`. `save_note_insight` is the only tool that changes data: it upserts an insight on a note you own and can overwrite an existing insight of the same type and target language, so it carries `destructiveHint: true`. Nothing is ever deleted. Every tool sets `openWorldHint: false` and `idempotentHint: true`.

Every tool declares an output schema and returns validated `structuredContent`, with the same JSON mirrored as text in `content` for clients that only read text.

### Identifiers

Notes carry two IDs. Use the right one for each tool:

| Field | Type | Used by |
|---|---|---|
| `id` | string | `get_note` (`note_id`) |
| `localRecordingId` | integer | `get_note_insights`, `get_note_labels`, `save_note_insight` (`local_recording_id`) |
| `localLabelId` | integer | `get_notes_by_label` (`local_label_id`) |

## Tool reference

### get_recent_notes

Lists notes and recordings, newest first.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `limit` | integer, 1 to 50 | No (default `10`) | Number of notes to return |
| `offset` | integer, 0 or more | No (default `0`) | Number of notes to skip, for pagination |
| `note_type` | `"audio"` \| `"text"` | No | Filter to voice recordings or written notes |

Returns `{ notes: Note[], hasMore: boolean }`.

```json
{ "limit": 5, "note_type": "audio" }
```

### search_notes

Keyword search across note titles, transcripts and text content.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `query` | string | Yes | Search keyword or phrase |
| `limit` | integer, 1 to 50 | No (default `10`) | Maximum number of results |

Returns `{ notes: NoteWithExcerpt[] }`. Each result adds an `excerpt` around the first match.

```json
{ "query": "Henderson site visit" }
```

### get_note

Returns one note with its complete transcript or text body.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `note_id` | string | Yes | The note's `id` |

Returns a `Note`.

### get_note_insights

Returns the AI insights saved against a note.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `local_recording_id` | string | Yes | The note's `localRecordingId` |
| `insight_type` | string | No | Filter, for example `summary`, `action_items`, `sentiment`, `translate` |

Returns `{ insights: Insight[] }`.

### get_user_labels

Lists every label on the account. Takes no parameters.

Returns `{ labels: { localLabelId: number, name: string }[] }`.

### get_note_labels

| Parameter | Type | Required | Description |
|---|---|---|---|
| `local_recording_id` | integer | Yes | The note's `localRecordingId` |

Returns `{ labels: Label[] }`.

### get_notes_by_label

| Parameter | Type | Required | Description |
|---|---|---|---|
| `local_label_id` | integer | Yes | `localLabelId` from `get_user_labels` |

Returns `{ notes: Note[] }`.

### get_subscription_status

Takes no parameters.

Returns `{ subscription: Subscription | null, isActive: boolean }`. `isActive` is true when the subscription is active or trialing and not expired.

### save_note_insight

Saves an insight against a note. Upserts on the combination of note, `insight_type` and `target_language`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `local_recording_id` | integer | Yes | The note's `localRecordingId` |
| `insight_type` | enum | Yes | One of the values below |
| `content` | string | Yes | The insight text |
| `target_language` | string | When `insight_type` is `translate` | Language code, for example `es`, `fr`, `de` |

Allowed `insight_type` values:

```text
summary, key_points, offer_tips, make_notes, action_items, meeting_report,
translate, linkedin_post, tweet, blog_post, email, sentiment, shopping_list
```

Returns the saved `Insight`.

```json
{
  "local_recording_id": 1712,
  "insight_type": "action_items",
  "content": "- Send revised quote by Friday\n- Book second site visit"
}
```

### Data shapes

```ts
interface Note {
  id: string                          // use with get_note
  localRecordingId: number            // use with insight and label tools
  displayName: string
  noteType: string                    // "audio" | "text"
  transcript: string | null           // audio notes
  textContent: string | null          // text notes
  duration: number | null             // seconds
  dateTime: string                    // ISO 8601
  transcriptionStatus: string | null
}

interface NoteWithExcerpt extends Note {
  excerpt: string | null              // snippet around the first match
}

interface Label {
  localLabelId: number
  name: string
}

interface Insight {
  id: string
  localRecordingId: string
  insightType: string
  content: string
  targetLanguage: string | null
}

interface Subscription {
  id: string
  provider: string                    // e.g. revenuecat, lemonsqueezy
  productId: string | null
  status: string                      // e.g. active, trialing, expired
  expiresAt: string | null            // ISO 8601, null if it does not expire
}
```

### Errors

A failed call returns a normal tool result with `isError: true` and a human-readable message in `content`. Internal details are stripped from the message. A call without a valid session returns `Authentication required`.

## What it can and cannot access

For IT administrators evaluating this connector.

**It can read:** your own notes and recordings, their transcripts and content, AI insights saved against them, your labels, and your subscription status.

**It can write:** one thing, an insight attached to a note you own.

**It cannot access:** any other user's data, Claude's memory, chat history, conversation summaries, or files you have uploaded to the assistant. It does not collect conversation data beyond what a given tool call needs to run.

**Scoping:** every database query is scoped to the authenticated user through Postgres row level security. A session cannot reach another account's data even if a tool were called with another account's identifiers.

## Security

Authentication uses OAuth 2.0 with Dynamic Client Registration (RFC 7591). The authorization server is Supabase Auth, with a consent page hosted on parrotnotes.app.

Every request to `/mcp` is verified against the auth server before it reaches the MCP layer, so a token that is expired, malformed, or whose session was signed out elsewhere is rejected immediately.

| Situation | Response |
|---|---|
| No bearer token | `401` with a `WWW-Authenticate` header pointing at the protected resource metadata |
| Token expired, malformed, or session revoked | `401` with `error="invalid_token"`, which tells the client to re-authenticate |
| Auth server unreachable | `503` with no challenge, so clients retry instead of re-authenticating |

Transport is HTTPS only. Tokens are accepted in the `Authorization: Bearer` header only.

## Privacy and data retention

- No data is used for AI model training.
- Audio auto-delete is available in the app: the transcript is kept and the recording is discarded. This matters in compliance-sensitive industries.
- Full policy: [parrotnotes.app/privacy](https://parrotnotes.app/privacy)

## Troubleshooting

**The assistant asks you to reconnect repeatedly.** Signing out of ParrotNotes on the web with a global scope ends every session for your account, including connector sessions. Sign in again and reauthorize the connector.

**Tools return empty results.** The account has no notes yet. Record or import something in the app first.

**A tool call fails with a permission error.** The note ID belongs to a different account. Tools only return data owned by the signed-in user.

**`get_note_labels` or `save_note_insight` rejects the ID.** These take the integer `localRecordingId`, not the string `id`. See [Identifiers](#identifiers).

## Support

- Email: [support@parrotnotes.app](mailto:support@parrotnotes.app)
- Privacy policy: [parrotnotes.app/privacy](https://parrotnotes.app/privacy)
- Terms: [parrotnotes.app/terms](https://parrotnotes.app/terms)
- This page as Markdown: [parrotnotes.app/docs/mcp.md](https://parrotnotes.app/docs/mcp.md)
