feat: add shared decks and multi-account Mastodon OAuth
This commit is contained in:
+87
-69
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user