MCP: calternal MCP server (remote, over HTTPS) — thin adapter over the HTTP API, scoped app passwords #329

Closed
opened 2026-09-28 10:06:17 +00:00 by kayg · 5 comments
Owner

Owner (2026-09-28): 'what happened to mcp?' DESIGN §41: calternal has four client paths (CLI, HTTP API, MCP, WebMCP); MCP was marked 'later' and was never filed. Filed now.

Build after the app-password scopes issue lands (it provides the mcp scope):

  • A remote MCP server at https://<instance>/mcp (Streamable HTTP transport per the current MCP spec), authenticated with an app password (Bearer) carrying the mcp scope. Evaluate MCP OAuth (the spec's authorization flow) as the better option for Claude/ChatGPT connectors, and recommend one in the report.
  • Thin adapter only (§41): every tool calls the existing HTTP route; no new write paths or permission checks. Tools mirror WebMCP's set first (search, open/read, today, list files, create log/task/note), plus read-only calendar range and file read. Writes are clearly marked in the tool descriptions; destructive tools are not exposed in v1.
  • The Rust implementation uses a permissively licensed MCP SDK (check the licence: no new copyleft deps per §43), or a small hand-written JSON-RPC layer if that is smaller.
  • Docs: how to add calternal to Claude (Desktop/Code) and ChatGPT as a connector, with the exact URL and app-password steps; Settings → App passwords has an 'AI assistant (MCP)' preset.
  • Tests: an MCP conformance/client test (e.g. the official inspector or SDK client in CI), scope enforcement, an adversarial round (oversized tool input, injection in arguments, cross-user access).
Owner (2026-09-28): 'what happened to mcp?' DESIGN §41: calternal has four client paths (CLI, HTTP API, **MCP**, WebMCP); MCP was marked 'later' and was never filed. Filed now. Build after the app-password scopes issue lands (it provides the `mcp` scope): - A **remote MCP server** at `https://<instance>/mcp` (Streamable HTTP transport per the current MCP spec), authenticated with an app password (Bearer) carrying the `mcp` scope. Evaluate MCP OAuth (the spec's authorization flow) as the better option for Claude/ChatGPT connectors, and recommend one in the report. - **Thin adapter only** (§41): every tool calls the existing HTTP route; no new write paths or permission checks. Tools mirror WebMCP's set first (search, open/read, today, list files, create log/task/note), plus read-only calendar range and file read. Writes are clearly marked in the tool descriptions; destructive tools are not exposed in v1. - The Rust implementation uses a permissively licensed MCP SDK (check the licence: no new copyleft deps per §43), or a small hand-written JSON-RPC layer if that is smaller. - Docs: how to add calternal to Claude (Desktop/Code) and ChatGPT as a connector, with the exact URL and app-password steps; Settings → App passwords has an 'AI assistant (MCP)' preset. - Tests: an MCP conformance/client test (e.g. the official inspector or SDK client in CI), scope enforcement, an adversarial round (oversized tool input, injection in arguments, cross-user access).
Author
Owner

Starting issue #329 on branch job/mcp, based on dev SHA 413ccaa7b7f7f509ec74176a20569db049ae6171.

Starting issue #329 on branch `job/mcp`, based on dev SHA `413ccaa7b7f7f509ec74176a20569db049ae6171`.
Author
Owner

Finding: the required docs/research/agent-conventions.md is on the #204 job commit 88441ac1b0f106f28a4aff8b5ec5554c217ac19d, not in this dev base. I read it from that commit. It recommends OAuth 2.1 + Protected Resource Metadata for hosted MCP clients and calls app-password authorization a separate implementation choice.

Client compatibility finding: current official ChatGPT docs expect OAuth 2.1 for authenticated MCP servers, and Claude's hosted remote connector flow offers OAuth setup. Claude Code supports a custom Authorization: Bearer header. The #329 requirement explicitly selects scoped app passwords, so I will implement that path, validate it with MCP Inspector, and document that ChatGPT and Claude hosted connectors need OAuth before they can use calternal directly. Sources: https://developers.openai.com/plugins/build/auth and https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp

Finding: the required `docs/research/agent-conventions.md` is on the #204 job commit `88441ac1b0f106f28a4aff8b5ec5554c217ac19d`, not in this `dev` base. I read it from that commit. It recommends OAuth 2.1 + Protected Resource Metadata for hosted MCP clients and calls app-password authorization a separate implementation choice. Client compatibility finding: current official ChatGPT docs expect OAuth 2.1 for authenticated MCP servers, and Claude's hosted remote connector flow offers OAuth setup. Claude Code supports a custom `Authorization: Bearer` header. The #329 requirement explicitly selects scoped app passwords, so I will implement that path, validate it with MCP Inspector, and document that ChatGPT and Claude hosted connectors need OAuth before they can use calternal directly. Sources: https://developers.openai.com/plugins/build/auth and https://support.claude.com/en/articles/11175166-get-started-with-custom-connectors-using-remote-mcp
Author
Owner

Finding for #329: the MCP route was classified as a write solely because Streamable HTTP uses POST for JSON-RPC. The new focused test failed at wire.rs:3922 for a read-only MCP app password calling POST /mcp, before the transport could classify tools/call. I changed the route gate to admit MCP POST when the credential has any MCP access, then check read/write access against the selected tool and audit denied calls there.

Finding for #329: the MCP route was classified as a write solely because Streamable HTTP uses POST for JSON-RPC. The new focused test failed at `wire.rs:3922` for a read-only MCP app password calling `POST /mcp`, before the transport could classify `tools/call`. I changed the route gate to admit MCP POST when the credential has any MCP access, then check read/write access against the selected tool and audit denied calls there.
Author
Owner

After the one-time dev merge, the single final cargo test gate stopped in calternal-cli/tests/output_contract.rs::connection_resets_retry_safe_read_requests. The test expected the safe whoami GET to be retried and complete after the local server dropped its first connection; observed one attempt and exit code 9 (error sending request for url). The adjacent transient-status retry test passed. I left the existing assertion and unrelated CLI/SYNC retry behavior unchanged. This is outside the MCP adapter path and needs owner follow-up.

After the one-time `dev` merge, the single final `cargo test` gate stopped in `calternal-cli/tests/output_contract.rs::connection_resets_retry_safe_read_requests`. The test expected the safe `whoami` GET to be retried and complete after the local server dropped its first connection; observed one attempt and exit code 9 (`error sending request for url`). The adjacent transient-status retry test passed. I left the existing assertion and unrelated CLI/SYNC retry behavior unchanged. This is outside the MCP adapter path and needs owner follow-up.
Author
Owner

Completed #329 on job/mcp. Head: ed173e4ad0985fbac865ada4631805eb61cc5c32 (includes the one required merge of dev; no merge into dev).

Built a stateless Streamable HTTP MCP endpoint at /mcp using RMCP 3.5.0 and protocol revision 2026-07-28. Seven tools map one-to-one to existing API routes. Each tool checks the app password's protocol and read/write scopes, reuses the authenticated User authority, and dispatches through the existing API route. Added request/response/concurrency caps, a client setup guide, and the MCP adversarial probe.

Files for this feature: crates/calternal-server/src/mcp.rs, crates/calternal-server/src/wire.rs, crates/calternal-server/src/main.rs, crates/calternal-server/Cargo.toml, Cargo.lock, docs/mcp.md, tests/adversarial/mcp_probe.py, tests/adversarial/run.sh, tests/adversarial/setup.mjs.

Decisions not specified by DESIGN: use stateless JSON responses with no session persistence; require modern MCP revision 2026-07-28 per request; cap requests at 128 KiB, responses at 1 MiB, and concurrent tool routes at 16. Reuse the MCP app-password protocol preset and existing API scopes. No new business logic was added.

Live local adversarial round, using official MCP Inspector 2.8.0:

MCP Inspector listed 7 tools and called search, create_log, and open.
MCP read scope denied create_log; API-only scope received HTTP 403.
Cross-User Note read was denied; oversized request received HTTP 413.
Malformed request returned HTTP 415; 48 parallel calls had no HTTP 5xx.

Gates (run once after merging dev):

cargo fmt --all -- --check
[no stdout or stderr; exit code 0]

cargo clippy --all-targets -- -D warnings
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 41s

cargo test
---- connection_resets_retry_safe_read_requests stdout ----
thread 'connection_resets_retry_safe_read_requests' (3050468) panicked at crates/calternal-cli/tests/output_contract.rs:772:5:
test connection_resets_retry_safe_read_requests ... FAILED
test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.49s
error: test failed, to rerun pass `-p calternal-cli --test output_contract`
[exit code 101]

bun run check
$ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/mcp/apps/web
Getting Svelte diagnostics...
svelte-check found 0 errors and 0 warnings

bun run test
 Test Files  118 passed (118)
      Tests  767 passed (767)
   Start at  03:47:08
   Duration  163.04s (transform 61%, environment 16%, import 13%, tests 8%, setup 2%)

Known gap/finding: the Rust workspace test gate stopped at the CLI retry test. It expected the safe whoami GET to retry after the local server dropped its first connection, but got exit code 9 after one attempt. The adjacent transient-status retry test passed. I left the existing expectation and unrelated CLI/SYNC retry behavior unchanged; the finding is recorded here for owner follow-up. Hosted ChatGPT/Claude connectors that require OAuth are not supported by the scoped app-password flow; clients that accept a custom Authorization header are supported.

Cleanup: cargo clean removed 18,552 files (16.7 GiB); removed apps/web/build and apps/web/.svelte-kit.

Completed #329 on `job/mcp`. Head: `ed173e4ad0985fbac865ada4631805eb61cc5c32` (includes the one required merge of `dev`; no merge into `dev`). Built a stateless Streamable HTTP MCP endpoint at `/mcp` using RMCP 3.5.0 and protocol revision `2026-07-28`. Seven tools map one-to-one to existing API routes. Each tool checks the app password's protocol and read/write scopes, reuses the authenticated User authority, and dispatches through the existing API route. Added request/response/concurrency caps, a client setup guide, and the MCP adversarial probe. Files for this feature: `crates/calternal-server/src/mcp.rs`, `crates/calternal-server/src/wire.rs`, `crates/calternal-server/src/main.rs`, `crates/calternal-server/Cargo.toml`, `Cargo.lock`, `docs/mcp.md`, `tests/adversarial/mcp_probe.py`, `tests/adversarial/run.sh`, `tests/adversarial/setup.mjs`. Decisions not specified by DESIGN: use stateless JSON responses with no session persistence; require modern MCP revision `2026-07-28` per request; cap requests at 128 KiB, responses at 1 MiB, and concurrent tool routes at 16. Reuse the MCP app-password protocol preset and existing API scopes. No new business logic was added. Live local adversarial round, using official MCP Inspector 2.8.0: ```text MCP Inspector listed 7 tools and called search, create_log, and open. MCP read scope denied create_log; API-only scope received HTTP 403. Cross-User Note read was denied; oversized request received HTTP 413. Malformed request returned HTTP 415; 48 parallel calls had no HTTP 5xx. ``` Gates (run once after merging `dev`): ```text cargo fmt --all -- --check [no stdout or stderr; exit code 0] cargo clippy --all-targets -- -D warnings Finished `dev` profile [unoptimized + debuginfo] target(s) in 12m 41s cargo test ---- connection_resets_retry_safe_read_requests stdout ---- thread 'connection_resets_retry_safe_read_requests' (3050468) panicked at crates/calternal-cli/tests/output_contract.rs:772:5: test connection_resets_retry_safe_read_requests ... FAILED test result: FAILED. 14 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.49s error: test failed, to rerun pass `-p calternal-cli --test output_contract` [exit code 101] bun run check $ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/mcp/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings bun run test Test Files 118 passed (118) Tests 767 passed (767) Start at 03:47:08 Duration 163.04s (transform 61%, environment 16%, import 13%, tests 8%, setup 2%) ``` Known gap/finding: the Rust workspace test gate stopped at the CLI retry test. It expected the safe `whoami` GET to retry after the local server dropped its first connection, but got exit code 9 after one attempt. The adjacent transient-status retry test passed. I left the existing expectation and unrelated CLI/SYNC retry behavior unchanged; the finding is recorded here for owner follow-up. Hosted ChatGPT/Claude connectors that require OAuth are not supported by the scoped app-password flow; clients that accept a custom Authorization header are supported. Cleanup: `cargo clean` removed 18,552 files (16.7 GiB); removed `apps/web/build` and `apps/web/.svelte-kit`.
kayg closed this issue 2026-09-29 03:37:55 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#329
No description provided.