feat: add shared decks and multi-account Mastodon OAuth

This commit is contained in:
2026-09-24 16:52:55 +09:00
parent d2cbf4dbd3
commit c47f58f065
100 changed files with 9215 additions and 1027 deletions
+87 -69
View File
@@ -1,88 +1,108 @@
# WebMCP prototype
## Scope and registration
## Registration
The deck workspace exposes seven React-owned tools on `/` and `/deck`.
`usewebmcp` owns native browser registration and cleanup. There is no polyfill
or external MCP transport. Unsupported browsers retain the manual UI. Tools
are enabled after local storage loads; relay-profile discovery may still be
pending, which `list_decks` reports as `profiles: null`.
The deck workspace exposes nine React-owned tools on `/` and `/deck`.
`usewebmcp` handles native browser registration and cleanup. Tools become
available after saved decks load. Connection discovery may still be pending;
`list_connections` then returns `connections: null`.
The former `search_posts`, `get_loaded_posts`, and `load_more_posts` tools and
standalone reader routes have been removed. Agents manage named decks and
address columns explicitly, including their bound relay profiles.
Registration uses `document.modelContext`; there is no polyfill or external
MCP transport. Unsupported browsers retain the manual interface. All server
operations use the same access controls and persistence rules as the UI.
## Workspace tools
| Tool | Input | Behavior |
| --- | --- | --- |
| `list_decks` | `{}` | Return all deck definitions, `activeDeckId`, available `profiles`, and `storageError` |
| `get_deck` | Optional `deckId` | Read a saved deck; omitted ID selects the active deck |
| `set_deck` | Optional `deckId`, required `title` and `columns` | Create when ID is omitted; otherwise replace an existing deck, then activate it |
| `select_deck` | `deckId` | Activate a saved deck and persist the selection |
| `delete_deck` | `deckId` | Permanently remove the definition; cannot delete the last deck |
| `list_connections` | `{}` | Return connection IDs, platforms, origins, account IDs, display names, and states; never credentials |
| `list_decks` | `{}` | Return saved decks and this tab's temporary views, `activeDeckId`, and `storageError` |
| `get_deck` | Optional `deckId` | Read the named or active view, including `persisted` and saved `revision` |
| `set_deck` | Optional `deckId` and `expectedRevision`, required `title` and `columns` | Without an ID, create a temporary view; with an ID, replace and activate that existing view |
| `save_deck` | `deckId` | Explicitly persist a temporary view; an already saved deck is unchanged |
| `select_deck` | `deckId` | Activate a view; selection remains device-local |
| `delete_deck` | `deckId`, optional `expectedRevision` | Discard a temporary view or delete a saved deck for all devices |
`set_deck` accepts at most six columns. Each requires `title`, `profileName`
from `list_decks`, and a discriminated `source`. Its `kind` is `search`, `user`,
or `list`; `platform` defaults to `twitter`. Searches require `query`, with
`product` defaulting to `Latest` and `following` to false. User and list sources
require `target` (handle/profile URL or list ID/URL). Unknown source fields,
invalid targets, duplicate column IDs, and unknown profiles fail before saving.
Read tools return the workspace's current client snapshot, not a fresh server
request. Saved definitions refresh on focus and while visible, except during
editing. Replacing or deleting a saved deck requires `expectedRevision` from
the definition being edited. A stale revision fails without overwriting the
server. Use the UI's reload action after a conflict when a fresh definition
is needed.
Read before editing. Include every column to keep; omitted columns are removed.
Preserve IDs for retained columns and omit IDs for new ones. An empty columns
array clears a deck. A supplied deck ID must already exist. Successful mutation
closes unsaved editor forms. `set_deck` returns the applied definition,
`persisted: true`, and `posts: "loading-asynchronously"`; searches can fail
independently after the save succeeds.
`set_deck` accepts at most six columns, each with a title, `connectionId` from
discovery, and a source. Connections must be connected and match the source
platform. Optional column IDs preserve identity; omitted IDs are generated.
Include every retained column: omitted columns are removed, and an empty array
clears the view. A supplied deck ID must already exist.
Deleting the active deck selects the first remaining one. Deletion has no
workspace-tool undo. Storage failures return `isError: true` explaining that
the mutation applied in memory but could not be persisted. Saving replaces
invalid or legacy saved data; there is no automatic legacy migration.
Twitter sources support `search`, `user`, and `list`. `platform` defaults to
`twitter`; search requires `query` and defaults `product` to `Latest` and
`following` to false. User/list targets accept handles or profile URLs and
numeric list IDs or list URLs, respectively.
Mastodon requires `platform: "mastodon"` and supports `search` (`query`),
`user` (handle or instance-local account ID), `list` (account-local numeric
list ID), and `hashtag` (tag without `#`). It does not accept Twitter ranking
or following controls. Search availability and coverage depend on the
instance; an empty successful result does not prove full-text support.
## Mutation results and persistence
`set_deck` returns `deck`, its actual `persisted` flag, and
`posts: "loading-asynchronously"`. A new view exists only in tab memory until
`save_deck` succeeds. Temporary views disappear on reload or navigation,
including OAuth redirects. Saving keeps the ID, allowing an identical create
request to be retried without duplicate decks. Failed saves keep temporary
content; rejected updates do not replace the accepted saved definition.
Successful mutations close unsaved editor forms. Deleting the active view
selects a remaining one; deleting the last opens an empty temporary view.
There is no tool-level deletion undo. Column requests may fail independently
after a definition is applied or saved. Existing browser decks are imported
only through the explicit UI import action, not through a tool side effect.
## Column tools
`get_column_posts` accepts `columnId`, `offset` (default 0, nonnegative integer),
and `limit` (default 20, integer 1–50). It reads already loaded posts without a
network request. Only columns mounted in the active deck are available; select
the deck and allow it to render first.
network request. Only columns mounted in the active view are available;
select the view and allow it to render first.
`load_more_column` accepts `columnId`. It loads or retries one continuation
using that column's bound profile. Wait for its initial load or refresh before
calling. Concurrent pagination joins the existing request. If the deck or
column changes during the request, the tool reports an error instead of
returning results under the new identity. At the end, it returns no appended
posts and `hasMore: false`.
`load_more_column` accepts `columnId` and fetches or retries one continuation
using the column's connection. Wait for initial loading or refresh to finish.
Concurrent pagination joins the existing request. If the view or column
changes during pagination, execution fails rather than returning results under
the changed identity. At the end, no posts are appended and `hasMore` is false.
Both return:
- `column`: ID, title, bound profile, and source definition
- `status`: `loading`, `ready`, or `error`, plus `loading` and `error` details
- `posts`: normalized records with original URLs, identity, text, author,
and available media/quotes
- `loadedCount`, `offset`, and `nextOffset` for slicing deduplicated cached posts
- `hasMore`: whether the current feed has an upstream continuation
- `column`: ID, title, connection ID, and source
- `status`: `loading`, `ready`, or `error`, with `loading` and `error` details
- `posts`: normalized records with original URLs, text, author, and available
media, quotes, content warnings, or boost information
- `loadedCount`, `offset`, and `nextOffset` for slices of deduplicated posts
- `hasMore`: whether the feed has an upstream continuation
Continuation returns up to 20 newly appended posts. Use `nextOffset` with
`get_column_posts` to read additional already loaded records, and
`load_more_column` for an upstream page. These are cache snapshots; manual UI
refreshes and pagination can change the available records.
`get_column_posts` for additional already loaded records; use
`load_more_column` for another upstream page. These are cache snapshots, not
archived evidence.
## Results and errors
## Errors and annotations
Tools return JSON in an MCP text content block. Execution failures set
`isError: true` with `code`, `message`, and `retryable`. Schema failures use
`invalid-input`; other tool failures use `tool-error`. Column snapshots report
underlying relay failures through their `error` field. An empty successful
query is not an error.
Results are JSON in an MCP text content block. Execution failures set
`isError: true` with `code`, `message`, and `retryable: false`. Schema and
connection-validation failures use `invalid-input`; other execution failures
use `tool-error`. Column snapshots expose upstream errors separately in their
`error` field. Empty successful queries are not errors.
`list_decks`, `get_deck`, and `get_column_posts` carry `readOnlyHint: true`.
The other tools change local UI or storage. `delete_deck` carries
`destructiveHint: true`; tools returning external posts mark them untrusted.
Annotations are metadata, not authorization controls. No tool writes to X.
`list_connections`, `list_decks`, `get_deck`, and `get_column_posts` have
`readOnlyHint: true`. `delete_deck` has `destructiveHint: true`; tools returning
external posts mark them untrusted. Annotations describe behavior and do not
grant authorization. No tool posts to or modifies an SNS account.
## Verification and browser setup
## Browser setup and verification
```sh
nix develop -c pnpm test
@@ -90,19 +110,17 @@ nix develop -c pnpm typecheck
nix develop -c pnpm test:e2e e2e/integrations/webmcp.test.ts
```
The E2E tests enable native Chromium WebMCP/testing flags and use
`navigator.modelContextTesting` to invoke actual registered tools. A mock
relay supplies deterministic responses through real server functions.
E2E tests enable Chromium WebMCP/testing flags and invoke actual registered
tools through `navigator.modelContextTesting`. Mock relay responses pass
through real server functions and isolated database state.
For interactive testing, enable `chrome://flags/#enable-webmcp-testing`,
restart Chrome, and use Model Context Tool Inspector on the app. Registration
uses `document.modelContext`; reload after changing browser support. Native
WebMCP requires a secure context: local loopback works for development; remote
Tailscale access should use an HTTPS Serve origin. Allow the exact hostname
through `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS` in the Vite process environment.
HTTP and HTTPS have separate browser-local workspaces.
restart Chrome, and use Model Context Tool Inspector. Reload after changing
browser support. Native WebMCP requires a secure context; use the configured
Tailscale Serve HTTPS origin for remote access. Development also requires
allowing its exact hostname through `__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS`.
The app's owner check still applies; direct localhost access does not supply
Serve identity. See [runtime access configuration](storage-and-oauth.md).
No production origin-trial token or external MCP-client bridge is configured.
Browser cancellation does not guarantee cancellation of a shared feed request.
Agent task-selection quality still needs evaluation with the consuming agent;
automated browser tests verify contracts and UI behavior.