Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
89f76e6
feat(mcp): Add plugin-scoped MCP tool support
dcramer Mar 17, 2026
14dfb62
fix(mcp): Preserve OAuth resume state
dcramer Mar 17, 2026
071d518
fix(security): Remove regex backtracking hotspots
dcramer Mar 17, 2026
6ead204
fix(mcp): Preserve loaded skills during auth pause
dcramer Mar 18, 2026
8d476f5
fix(mcp): Close all clients during teardown
dcramer Mar 18, 2026
4a492cb
fix(mcp): Preserve resumed file uploads
dcramer Mar 18, 2026
3ade506
feat(mcp): Dispatch plugin tools through stable host tools
dcramer Mar 18, 2026
4d725ed
chore(example): Update next-env route types import
dcramer Mar 18, 2026
70394e1
ref(mcp): Trim manager and auth store interfaces
dcramer Mar 18, 2026
079b67c
fix(oauth): Stop echoing callback errors in HTML
dcramer Mar 18, 2026
105f286
fix(mcp): Park handled auth pauses cleanly
dcramer Mar 18, 2026
fd49285
fix(mcp): Preserve loaded skill metadata
dcramer Mar 19, 2026
2968c09
ref(oauth): Simplify auth resume flow
dcramer Mar 19, 2026
361a470
test(logging): Cover escaped PEM redaction
dcramer Mar 19, 2026
c89a315
fix(mcp): Clear cached tools after session reset
dcramer Mar 19, 2026
281ee96
ref(mcp): Simplify provider tool ownership
dcramer Mar 19, 2026
4309ec7
ref(mcp): Clarify auth pause snapshot timing
dcramer Mar 19, 2026
8adcc78
fix(mcp): Handle auth challenges during connect
dcramer Mar 19, 2026
b31c536
fix(mcp): Preserve auth resume on checkpoint failures
dcramer Mar 19, 2026
5a7b11b
fix(logging): Remove enduser.id from console priority keys
dcramer Mar 19, 2026
d0bbfe8
fix(mcp): Keep completed turns after auth races
dcramer Mar 20, 2026
d85e877
fix(mcp): Restore auth resume checkpoint state
dcramer Mar 20, 2026
c40e3e8
fix(oauth): harden auth resume eval coverage
dcramer Mar 20, 2026
1b23e46
test(oauth): trim mock-heavy auth coverage
dcramer Mar 21, 2026
1c7f31f
fix(oauth): Reuse pending MCP auth sessions
dcramer Mar 21, 2026
e91b6dc
fix(mcp): Avoid duplicate auth activation retries
dcramer Mar 21, 2026
3101387
test(msw): Match unhandled request contract
dcramer Mar 21, 2026
b06748f
fix(mcp): Preserve raw transport 401 failures
dcramer Mar 21, 2026
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: 1 addition & 1 deletion apps/example/next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
"scripts": {
"dev": "node scripts/dev-with-root-env.mjs",
"cli": "node scripts/cli-with-root-env.mjs",
"notion:search": "node packages/junior-notion/scripts/run-with-root-env.mjs ../skills/notion/scripts/notion-cli.mjs search",
"notion:fetch": "node packages/junior-notion/scripts/run-with-root-env.mjs ../skills/notion/scripts/notion-cli.mjs fetch",
"prepare": "simple-git-hooks",
"lint-staged": "lint-staged",
"build": "pnpm --filter @sentry/junior build",
Expand Down
1 change: 1 addition & 0 deletions packages/docs/src/content/docs/extend/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ runtime-postinstall:
- `runtime-dependencies`: sandbox dependencies required by the plugin’s tools
- `runtime-postinstall`: commands that run after dependency install and before snapshot capture
- `mcp`: optional MCP server configuration for provider-scoped tool sources
- `mcp.allowed-tools`: optional raw MCP tool-name allowlist when a plugin should expose only part of a provider's tool surface

### Add skills to the plugin

Expand Down
68 changes: 25 additions & 43 deletions packages/docs/src/content/docs/extend/notion-plugin.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
---
title: Notion Plugin
description: Configure a shared internal Notion integration for read-only page and data source search workflows.
description: Configure the hosted Notion MCP server for read-only page and data source search workflows.
type: tutorial
summary: Install the Notion plugin, register it with withJunior, configure a shared integration token, and verify Notion search workflows.
summary: Install the Notion plugin, register it with withJunior, connect user accounts through Notion MCP OAuth, and verify read-only search workflows.
prerequisites:
- /extend/
related:
- /concepts/credentials-and-oauth/
- /operate/security-hardening/
---

The Notion plugin uses a shared internal integration so Slack users can search shared Notion pages and data sources through normal Notion requests.
The Notion plugin uses Notion's hosted MCP server so Slack users can search and fetch content from their own Notion account context.

Notion's public search API is more limited than the search experience in the Notion app. Junior uses the stable public API, so Notion requests work best when users search for the exact page or data source title they want to open.
Junior intentionally keeps this plugin read-only. It exposes only Notion's `notion-search` and `notion-fetch` MCP tools, even though the hosted server supports write-capable tools.

Notion search is still title-biased. Requests work best when users search for the exact page or data source title they want to open.

## Install

Expand All @@ -34,55 +36,35 @@ export default withJunior({
});
```

## Configure environment variables

Set these values in the host environment:

| Variable | Required | Purpose |
| -------------- | -------- | -------------------------------------------------------------------- |
| `NOTION_TOKEN` | Yes | Internal integration secret used for search and page fetch requests. |

## Create the Notion integration

Start with Notion's [Authorization guide](https://developers.notion.com/guides/get-started/authorization), then create an internal integration in the Notion integrations dashboard.

After you create the integration:
## Auth model

1. Choose the workspace where the integration will live.
2. Open the `Capabilities` tab and enable `Read content`.
3. Open the `Configuration` tab and copy the integration secret.
4. Store that secret in your deployment environment as `NOTION_TOKEN`.
- No `NOTION_TOKEN` or shared integration secret is required.
- Each user completes OAuth the first time Junior calls a Notion MCP tool on their behalf.
- Junior sends the authorization link privately, then resumes the same thread automatically after the user authorizes.
- Notion MCP requires user-based OAuth and does not support bearer token authentication, so this plugin is not suitable for fully headless automation.

## Share pages and data sources with the integration
## What users can do

Notion internal integrations only see the pages and data sources that are explicitly shared with them:

1. Open the page or data source in Notion.
2. Click the `•••` menu in the upper right.
3. Choose `Add connections`.
4. Select your integration.

This is the most common reason a Notion request returns no matches or a permission-style error.
- Search for a page or data source by title-style query.
- Fetch the best matching result and summarize its content.
- Disconnect their account later from Junior App Home with `Unlink`.

## Verify

Confirm the token is set, the target content is shared with the integration, and a real search succeeds:

- Ask Junior to search Notion for a real page or data source title and confirm the response includes the expected result.
- If needed, verify the same content through the local helper scripts:
Confirm a real user can connect and search successfully:

```bash
pnpm notion:search -- --query "company holidays"
pnpm notion:fetch -- --id "<notion-id>" --object page
```
1. Ask Junior to search Notion for a real page or data source title.
2. Complete the private OAuth flow when Junior prompts for it.
3. Confirm the thread resumes automatically and includes the expected Notion result.
4. Open Junior App Home and confirm Notion appears under `Connected accounts`.

## Failure modes

- No search matches: the target page or data source is not shared with the integration yet, or Notion search is still indexing newly shared content. Share the content directly and retry after indexing catches up.
- `403` from Notion: the integration is missing `Read content`. Enable that capability in the integration settings.
- `401` from Notion: `NOTION_TOKEN` is missing or invalid. Update the deployment secret and redeploy.
- Retrieval errors after a match: the matching page or data source could not be fetched for summarization. Confirm the object is still shared and accessible to the integration.
- Search results differ from notion.so: Junior uses Notion's public `v1` API, which is title-biased and does not expose the richer `Best matches` behavior from the Notion UI. Search by the exact title when possible.
- No auth prompt or no resume: the user still needs to complete the OAuth flow. Retry the request and finish the private authorization flow when prompted.
- No search matches: the query is too broad, the content is outside the user's Notion permissions, or search has not indexed recent changes yet.
- Search results differ from notion.so: MCP search is still title-biased. Search by the exact title when possible.
- Connected-source results are missing: search across Slack, Google Drive, or Jira requires a Notion AI plan. Without it, search is limited to the user's Notion workspace.
- Retrieval errors after a match: the matching page or data source could not be fetched for summarization. Confirm the user can still access that object in Notion.

## Next step

Expand Down
42 changes: 13 additions & 29 deletions packages/junior-notion/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
# @sentry/junior-notion

`@sentry/junior-notion` adds read-only Notion search workflows for pages and data sources to Junior via a shared internal Notion integration.
`@sentry/junior-notion` adds read-only Notion search workflows for pages and data sources to Junior through Notion's hosted MCP server.

Install it alongside `@sentry/junior`:

```bash
pnpm add @sentry/junior @sentry/junior-notion
```

Create an internal Notion integration by following Notion's Authorization guide:

- https://developers.notion.com/guides/get-started/authorization

In the Notion integration settings:

- choose the workspace where the integration will live
- enable the `Read content` capability
- copy the integration secret from the `Configuration` tab
- share any pages or data sources Junior should read via `•••` -> `Add connections`

Set that value in your host environment:

- `NOTION_TOKEN`

Then register the plugin package in `withJunior(...)`:

```js
Expand All @@ -33,24 +18,23 @@ export default withJunior({
});
```

There is no `/notion auth` flow for this plugin. Once the token is configured and pages or data sources are shared with the integration, users can run `/notion <query>` directly.
This package does not use `NOTION_TOKEN` or a shared workspace integration. Each user connects their own Notion account the first time Junior calls a Notion MCP tool. Junior sends the OAuth link privately and resumes the thread automatically after the user authorizes.

## Search limitations
Junior intentionally keeps this package read-only by exposing only Notion's `notion-search` and `notion-fetch` MCP tools. The plugin does not expose create, update, move, or other write-capable Notion tools.

This plugin currently uses Notion's public `v1` API for search and content retrieval.
## Search limitations

- `v1/search` is title-biased and does not match the richer `Best matches` behavior users see in notion.so.
- Results can differ from the UI even when the user can see a page in the Notion app.
- The most common cause of missing results is that the target page or data source is not directly shared with the integration.
- Newly shared content can also lag behind search indexing.
This package uses Notion MCP search and fetch rather than the older REST helper flow.

We also tested Notion's private `api/v3/search` endpoint with the same integration token. It accepted the token at the HTTP layer, but it did not return useful results for the same sample queries, so this plugin does not depend on `api/v3`.
- Search is still title-biased, so prompts work best when users search for the actual page or data source title.
- Results can differ from the Notion UI even when the user can see a page in the app.
- Search across connected sources like Slack, Google Drive, and Jira requires a Notion AI plan. Without Notion AI, search is limited to the user's Notion workspace.
- Missing results are usually a permissions problem on the user's Notion account or a weak query phrase.

For local debugging, the package exposes one Notion helper script through two subcommands that load the workspace env first:
## Auth model

```bash
pnpm notion:search -- --query "company holidays"
pnpm notion:fetch -- --id "<notion-id>" --object page
```
- Notion MCP requires user-based OAuth and does not support bearer token authentication.
- This package is not suitable for fully headless or unattended automation.
- Users can disconnect from Junior App Home with `Unlink`, or by asking Junior to disconnect Notion.

Full setup guide: https://junior.sentry.dev/extend/notion-plugin/
16 changes: 6 additions & 10 deletions packages/junior-notion/plugin.yaml
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
name: notion
description: Notion page search and summarization

capabilities:
- api

credentials:
type: oauth-bearer
api-domains:
- api.notion.com
api-headers:
Notion-Version: "2025-09-03"
auth-token-env: NOTION_TOKEN
mcp:
transport: http
url: https://mcp.notion.com/mcp
allowed-tools:
- notion-search
- notion-fetch
45 changes: 0 additions & 45 deletions packages/junior-notion/scripts/run-with-root-env.mjs

This file was deleted.

47 changes: 27 additions & 20 deletions packages/junior-notion/skills/notion/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
---
name: notion
description: Search Notion pages and data sources and summarize the best match. Use when users ask to look up docs, specs, notes, meeting notes, project context, roadmaps, trackers, or internal references stored in Notion.
requires-capabilities: notion.api
allowed-tools: bash
---

# Notion Operations
Expand All @@ -13,36 +11,45 @@ Use this skill for `/notion` workflows in the harness.

1. Classify the request:

- `auth`: explain that this plugin uses a shared internal Notion integration, so there is no per-user auth flow. Tell the user the workspace admin must configure `NOTION_TOKEN` and share the relevant pages or data sources with the integration.
- `disconnect`: explain that there is no per-user Notion connection to remove because the plugin uses a shared internal integration.
- `disconnect`: run `jr-rpc delete-token notion` in `bash`, then confirm that the user's Notion connection was removed.
- otherwise treat the request as a read-only query.

2. Enable credentials:
2. Keep tool work mostly silent:

- Before any Notion API call, run `jr-rpc issue-credential notion.api`.
- If credential issuance fails, explain that Notion is not configured on the host and the admin must set `NOTION_TOKEN`.
- Send at most one short acknowledgment before Notion tool work.
- Keep intermediate search/fetch reasoning internal.
- Do not narrate each step with "let me...", "I found...", or partial findings while tools are still running.
- Reply with the real answer once you have enough evidence, or explain the actual blocker if you cannot finish.

3. Search with the checked-in helper:
3. Search with MCP:

- Do not improvise `curl` requests or inline `node` snippets for Notion.
- `loadSkill` returns `available_tools` for this skill, including the exact `tool_name` values and argument schemas for the Notion tools exposed in this turn.
- Use `useTool` with those exact `tool_name` values.
- Use `searchTools` only if you need to rediscover or filter the active Notion tools later in the turn.
- The first MCP call may trigger a private OAuth link. Do not try to start auth manually. The runtime will pause and resume automatically after the user completes the flow.
- Decide the actual search phrases first. Notion search is title-biased, so search for the likely page or data source title, not the user's full sentence.
- Use 1-3 short explicit search phrases.
- Good: `deployment pipeline`, `launch tracker`, `incident review`
- Bad: `how do we handle deployment pipelines for mobile releases`
- Run search with the loaded `skill_dir` path:
`node <skill_dir>/scripts/notion-cli.mjs search --query "<best phrase>" --query "<fallback phrase>"`
- If the first phrase misses, rerun with 1-2 alternate title-style phrases.
- Search returns ranked page/data-source candidates only. Pick the best candidate, then fetch content with:
`node <skill_dir>/scripts/notion-cli.mjs fetch --id "<result id>" --object "page"`
or
`node <skill_dir>/scripts/notion-cli.mjs fetch --id "<result id>" --object "data_source"`
- Use the fetch output to summarize page markdown, or summarize the data source schema and returned rows.
- For list/report/calendar requests, search for the canonical container first:
- page title: `holidays`, `company holidays`
- data source title: `people calendar`
- Prefer one refinement round at most. If the first search already found a plausible canonical page or data source, fetch it before searching again.

4. Fetch efficiently:

- Search returns ranked page and data-source candidates only. Pick the best candidate, then fetch content with the disclosed Notion fetch tool via `useTool` using the returned URL or ID.
- If a fetched page clearly points at an inline data source or database, fetch that data source next and work from it.
- If the fetched data source already contains the rows and fields needed to answer, stop there and answer from that result.
- Do not serially fetch many individual row pages when the container page or data source already exposes the needed fields.
- Fetch individual rows only when a small number of important fields are still missing or ambiguous after fetching the canonical page or data source.
- Once you have enough evidence to answer, stop fetching and respond.

## Guardrails

- Read-only only.
- Do not print credential values.
- The runtime injects `Authorization` and `Notion-Version`; the helper handles request-specific headers.
- Junior intentionally exposes only Notion search and fetch tools for this skill. Do not ask for writes, comments, or page moves.
- Search results may be pages or data sources. Do not treat data sources as unsupported.
- If search returns no accessible matches, say that no accessible pages or data sources matched and note the content may not be shared with the integration yet.
- For scoped requests like "US holidays" or "2026 holidays", apply the user's scope when reading the fetched content and state any assumption you made if the source mixes multiple geos or years.
- If search returns no accessible matches, say that no accessible pages or data sources matched and note that the content may be outside the user's Notion permissions or poorly matched by title.
- If content retrieval fails for the top result, return the best matching Notion URL and explain that the result could not be fetched for summarization.
Loading
Loading