Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ jobs:
cache: pnpm

- run: pnpm install --frozen-lockfile
- run: pnpm --dir apps/pwa install --frozen-lockfile

# typecheck / test default to `pnpm run --if-present <script>` so a repo
# that hasn't defined these scripts yet gets a green workflow instead of
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ jobs:
registry-url: https://registry.npmjs.org

- run: pnpm install --frozen-lockfile
- run: pnpm --dir apps/pwa install --frozen-lockfile

# Publishing is the one action here that cannot be taken back — npm will
# not let a version be replaced — so the tests run first, on the exact
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ jobs:
cache: pnpm

- run: pnpm install --frozen-lockfile
- run: pnpm --dir apps/pwa install --frozen-lockfile

# test defaults to `pnpm run --if-present test` so a repo that hasn't
# defined a test script yet gets a green workflow instead of failing on
Expand Down
40 changes: 34 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1591,9 +1591,9 @@ session:

```sh
moshcode
/mcp answer # default: all session scopes for 8 hours
/mcp answer # default: read-only for 8 hours
/mcp answer --ttl 30m # shorter share
/mcp answer --scope sessions:read,sessions:control
/mcp answer --scope sessions:read,sessions:write,sessions:approve,sessions:cancel
```

Paste the printed `https://moshcode.sh/api/v1/mcp/mcs_…` endpoint into a remote
Expand All @@ -1602,10 +1602,28 @@ specific session and scopes in the browser, and its access token is bound to
that one opaque share URL. OAuth authorization code + PKCE, rotating refresh
tokens, and device authorization are supported.

The shared server exposes `moshcode_session_read`, `moshcode_session_answer`,
`moshcode_session_approve`, `moshcode_session_send`, and
`moshcode_session_cancel`. Read and control access are separate scopes; the
authorization page shows exactly which ones the client requested. Share
- **ChatGPT:** in an account/workspace with custom MCP apps enabled, open
Settings → Apps → Create, enter the share URL, choose OAuth, and scan tools.
Complete the Moshcode consent page as the session owner. Availability and
write permissions depend on your plan and administrator settings; see
[OpenAI's setup guide](https://help.openai.com/en/articles/12584461-developer-mode-and-mcp-apps-in-chatgpt-beta).
- **Claude:** open Customize → Connectors → Add custom connector, enter the
share URL, then connect and complete Moshcode authorization. Team owners
configure the connector for their organization first. See
[Claude's setup guide](https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp).
- **Chovy and other integrations:** use an OAuth-capable remote MCP client
with the same share URL. Chovy does not yet provide a native connector screen;
its adapter needs the discovery, PKCE, resource-bound token and refresh flow
described in [the server integration contract](apps/pwa/MCP.md).

After connecting, ask the client to read this session before granting or using
write tools. Removing a client-side connector does not replace revoking the
Moshcode share when you want to end all access to that URL.

The shared server exposes `session_read`, `session_answer`, `session_approve`,
`session_send`, and `session_cancel`. Reading, writing, approval and interruption
have separate scopes; the authorization page shows which ones the client
requested. Each tool is bound to the shared session. Share
management stays with the logged-in Moshcode operator:

```sh
Expand All @@ -1619,6 +1637,16 @@ moshcode mcp revoke mcs_…
writes fail, and revoking or expiring the share invalidates its access and
refresh tokens.

If you started the pit before signing in, `/mcp answer` signs you in and starts
its live connection. `/mcp connect` also reconnects the current pit after login.
An unreachable service can be retried without restarting the terminal. An
explicit `MOSHCODE_NO_MIRROR` setting remains respected; unset it and restart
before sharing. Remote approval sends a bounded yes/no to the terminal; it does
not identify a particular engine prompt. Read the current output before acting.
The write scope permits arbitrary terminal input, including commands and a
typed "yes". The approval scope restricts the approval tool; it does not block
someone who already has write permission from answering a prompt themselves.

### Known MCP servers

Some MCP servers are worth remembering by name rather than by npx invocation:
Expand Down
102 changes: 102 additions & 0 deletions apps/pwa/MCP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Remote session MCP

The canonical resource is `https://moshcode.sh/api/v1/mcp/<opaque-share-id>`.
OAuth remains on `https://app.moshcode.sh`. The front door must proxy the share
endpoint, owner share-management endpoints, and per-share protected-resource
metadata without changing resource identifiers. `MCP_PUBLIC_ORIGIN` controls the
share host; `PUBLIC_ORIGIN` remains the existing account and OAuth authority.

New shares default to `sessions:read`. Request additional permissions explicitly:

| Scope | Bound-share tools |
| --- | --- |
| `sessions:read` | `session_read` |
| `sessions:write` | `session_send`, `session_answer` |
| `sessions:approve` | `session_approve` (`approve` or `deny` only) |
| `sessions:cancel` | `session_cancel` |

Write, approval and cancellation also include read access. A share automatically
binds tools to its session; a conflicting `session_id` is rejected. Legacy
`moshcode_session_*` aliases for those five tools remain accepted. Other legacy
tools, including navigation keys, are unavailable on a share. `/mcp` retains its old tool
names and `sessions:control` compatibility. Broad control is rejected for new
share grants. Migration 022 converts existing share control consent to the
same explicit permissions. Existing tokens for the same owner, client, resource
and session are grouped conservatively for replay revocation because previous
rotations did not preserve token lineage.

These are terminal operations. Write access can type any bounded line, including
an answer to a confirmation. Separate tool scopes do not turn raw terminal bytes
into prompt-specific authorization. Approval queues `yes` or `no`; it does not
claim to identify or correlate a particular engine confirmation. Input is
limited to 50 lines of 500 characters each, with terminal control bytes rejected.

OAuth code grants require S256 PKCE, the registered redirect, the registered
client and the exact resource. Authorization codes are single-use. Device and
refresh token requests must repeat the exact share `resource`; device polls honor
the advertised interval and increase it by five seconds after `slow_down`.
Only the session owner can consent, through the existing authenticated,
CSRF-protected browser flow. Device secrets are stored hashed. Device flows
started before the storage upgrade should be restarted after deployment.

Access tokens last at most one hour and never outlive the share. Refresh tokens
rotate; replay revokes the entire authorization grant, including replacement
access/refresh tokens. `POST /oauth/revoke` accepts `token` and `client_id` and
revokes that grant without revealing whether an unknown token existed. Share
revocation and expiry invalidate all access and further token issuance.

Queued actions carry both share and OAuth grant provenance. The CLI's atomic
queue claim rechecks the share's owner, session, expiry and revocation, and the
grant's revocation state. Share revocation, OAuth revocation and refresh replay
cancel still-queued actions; an action already claimed by the CLI may finish. Revocation cannot
recall input already delivered to a terminal. The queue's `{id, body}` wire
format is unchanged.

`mcp_audit_events` records identities, fixed action names, outcomes and time.
Command/answer text, terminal output and credentials are absent from audit rows.
The operational command queue and output mirror still contain the content they
must deliver; they are not audit storage.

The HTTP transport uses JSON responses to authenticated POST requests. GET
performs OAuth discovery/challenge and returns 405 for an authenticated client
because the server does not offer a standalone SSE stream. Metadata and machine
preflight responses support CORS; an authenticated browser MCP request must have
a configured origin or one of that OAuth client's registered redirect origins.
Unknown protocol versions and tool calls disguised as notifications are rejected.

## Client adapter contract

ChatGPT and Claude setup links are in the root README. A Chovy adapter or another
remote MCP client should accept the exact share URL, discover its protected
resource metadata from the 401 challenge, and follow `authorization_servers`
to the app authority. Register a redirect URI with the advertised registration
endpoint, generate a random state and S256 PKCE verifier, and open the advertised
authorization endpoint with the share URL as `resource`. Validate the callback
state, then exchange the single-use code using the same redirect, verifier,
client ID and resource. Store tokens privately on behalf of that user.

Send the access token only to the canonical share resource. Initialize using a
supported MCP protocol version, list the tools allowed by the granted scope,
and call them with the bound session implicit. Refresh through the advertised
token endpoint, replacing both stored tokens atomically and repeating the exact
resource. A refresh failure requires reauthorization; never retry a consumed
refresh token. Revocation or expiry must stop further tool calls. Disconnect can
revoke the authorization grant through the advertised revocation endpoint.

This is an integration contract, not a claim that Chovy already has a native
remote MCP connector. Hosted ChatGPT/Claude account flows require their own
account access to verify; local protocol and terminal tests do not establish
that a particular hosted account or workspace policy permits a connector.

Validation uses isolated local databases and HTTP servers, including ownership,
CSRF, scope separation, exact binding, replay, concurrency, queue revocation,
audit exclusions and an upgrade from the previous database schema:

```sh
cd apps/pwa
node --test test/mcp-*.test.mjs
```

References: [MCP authorization and resource binding](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization),
[RFC 8628 device polling](https://www.rfc-editor.org/rfc/rfc8628.html#section-3.5),
[RFC 9700 refresh-token protection](https://www.rfc-editor.org/rfc/rfc9700.html#section-4.14).
16 changes: 16 additions & 0 deletions apps/pwa/src/lib/mcp-audit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { run } from "../db.mjs";
import { id } from "./crypto.mjs";

/** Callers pass identity and a fixed action/outcome only, never request bodies. */
export async function auditMcp({ userId, clientId = null, shareId = null, sessionId = null, action, outcome }) {
const eventId = id();
await run(
`INSERT INTO mcp_audit_events (id,user_id,client_id,share_id,session_id,action,outcome,created_at)
VALUES (?,?,?,?,?,?,?,?)`,
[eventId, userId, clientId, shareId, sessionId, action, outcome, Date.now()]
);
return eventId;
}

export const finishMcpAudit = (eventId, outcome) =>
run(`UPDATE mcp_audit_events SET outcome=? WHERE id=?`, [outcome, eventId]);
Loading
Loading