- TypeScript 100%
| src | ||
| .gitignore | ||
| LICENSE | ||
| package-lock.json | ||
| package.json | ||
| README.ja.md | ||
| README.md | ||
| tsconfig.json | ||
StudyNext UserInput MCP (independent global repository)
This standalone stdio MCP server reads the fixed public StudyNext board and offers a tightly limited moderator action: append an app.userinput.status sidecar record to the board owner's repository. It does not edit or delete discussion posts. Status records are additive; the board honors authorized owner/moderator records and latest createdAt. Cross-moderator updates have no global compare-and-swap guarantee.
This repository is intended to be cloned and run by its users; it is not a hosted service or a general-purpose MCP for arbitrary boards. The board, owner, moderator PDS mapping, and PDS endpoint are fixed in source. The published DIDs and service URLs are operational identifiers, not credentials. Review the source and confirm you intend to access this public board and authenticate as its owner before setup.
Safety model and permissions
- The only write is
createRecordwith repository fixed todid:plc:nstib2qjcu3tyun7cxz4dxcmand collection fixed toapp.userinput.status. It obtains the owner's current commit CID using the standard unauthenticatedcom.atproto.sync.getLatestCommitHTTPS GET on the pinned PDS and supplies that CID asswapCommittocreateRecord. AnInvalidSwapstops with no retry. This compare-and-swap protects owner-repository commits only; it cannot serialize moderator records in different repositories. There is no generic XRPC, NSID, repository, or social-write tool. - All authenticated
AtpAgentrequests use a fixed-PDS fetch wrapper with a 15-second abort timeout, caller-signal composition, and redirect rejection; no arbitrary host can be fetched or receive a bearer token by redirect. Refresh timeouts are transient and do not trigger App Password login. AcreateRecordtimeout remains indeterminate, is read back once best-effort, and is never retried. - Canonical status reads come from public
app.userinput.statussidecars in the board owner's and current moderators' PDS repositories; the public board API's aggregatestatusis retained only asrawAggregateStatusfor diagnostics. The fixed moderator DID-to-PDS allowlist is insrc/server.ts; if the board lists an unknown moderator or a PDS read/pagination is incomplete, status-dependent tools fail closed. Update and verify the allowlist if moderators or their PDS hosts change. - The sole write tool is
userinput_update_status, takinguri,state,reason, and optional duplicate target URI/CID. Target must be a currentapp.userinput.discussionURI whose recordspace.uriequals the exact board URI. It re-reads the board immediately before writing, checking post CID and canonical/raw status snapshots. Same-state changes are rejected. A timeout is never retried; owner repo and board are read best-effort to help determine outcome. - A missing canonical status is displayed and filtered as effective
open, matching the board's unassigned default. The raw status snapshot is compared for stale-state checks; malformed non-null sidecars are not treated as open. Equal-createdAt records for a subject are ambiguous and cause a fail-closed error rather than an arbitrary winner. - State is the nine-value lexicon enum. Duplicate status requires an exact current-board discussion URI/CID. A nonempty reason is required and becomes
note, max 1500 graphemes and 3000 code units. After writing, the server verifies the created owner-PDS record directly, then fetches the public board aggregate once as a diagnostic. It does not rescan every moderator repo after writing; the aggregate may lag or reflect a concurrent moderator and does not supersede the direct PDS record confirmation. - PDS is pinned to
https://pds2.soverth.blue, and setup checks both the current public PLC DID document identity and that it binds the pinned HTTPS endpoint. Login must authenticate exactly as the owner DID. Setup leaves the created owner session active; it does not revoke the saved session. - AT Protocol App Passwords have broad PDS account rights and are not collection-scoped. Do not use an account password. Enter a dedicated App Password only at the hidden TTY prompt. For keychain storage it is passed over child stdin to
/usr/bin/secret-tool; it is never placed in command arguments, environment, logs, config, or a file. Secret Service uses fixed application and owner-DID attributes. There is no plaintext fallback. Secret Service/keyring contents are accessible to the same OS user and do not isolate the secret from same-UID processes/shell. - Session directory is
~/.local/share/opencode/studynext-userinput(0700), session file 0600, atomic same-directory temp/fsync/rename; symlinks, wrong owner, and permissive paths are rejected. The session remains accessible to processes running as the same OS user; this is residual risk, not OS-level isolation. Do not run this under shared/untrusted shell accounts. - The resumed
AtpAgentis cached per server process. Because@atproto/api@0.19.18does not await refresh callbacks, callbacks are serialized and an explicit flush waits for persistence after every authenticated XRPC and before the tool responds. A persistence failure is returned as an explicit partial success/error with record read-back and board effectiveness stated separately. If the write may have succeeded, inspect the owner's status repo and public board; do not repeat that status action. Recover the session withnpm run setup, then reconnect/restart the MCP server; do not attempt another update until recovery. The server never automatically replays a status write. If the local session is missing, or an invalid/expired refresh JWT is detected, it may look up the dedicated password in Secret Service and attempt one automatic login before the next action. If expiry occurs during an XRPC, that request is not replayed; the next tool request performs recovery first. Transient/network refresh errors do not trigger login; an unsuccessful automatic login blocks further attempts for that process, requiringnpm run setupand an MCP reconnect. Separate MCP server processes still share one rotating session file and have no cross-process refresh lock; avoid running multiple instances concurrently. - No feedback text is written to Memory MCP or another store.
Install and use
Requires Node.js 20+ and Linux Secret Service support (/usr/bin/secret-tool, typically provided by libsecret-tools, with an unlocked Secret Service keyring). From the cloned repository directory, install dependencies and run the mock-only suite, build, and typecheck:
npm ci
npm test
npm run build
npm run typecheck
npm run setup # interactive TTY; App Password prompt is hidden
npm start # stdio MCP
npm run revoke-session # remove local rotating session only
npm run forget-auth # remove the Secret Service password and local session
Setup validates owner identity, authenticates, then saves the dedicated App Password to Secret Service and the rotating session to the protected local file. If Secret Service storage fails, no local session is saved and remote logout is attempted best-effort. If setup succeeded before keychain support was installed, rerun npm run setup with the same dedicated App Password to add it to Secret Service. npm run revoke-session removes only the local refresh session and retains the Secret Service App Password, so the next authenticated use may auto-login; it does not call PDS deleteSession or revoke an already-issued server session. npm run forget-auth clears the fixed Secret Service entry and local session, disabling that fallback; it also does not revoke a server session already issued. Revoke the App Password separately in account security settings; if exposed, revoke it immediately and create a replacement. Never paste an App Password into chat, a shell command, environment, config, or Git. If guaranteed server-side session revocation is needed, use the PDS/account revocation controls; this MCP does not implement that flow.
OpenCode registration (example only; no config is changed)
After npm ci and npm run build, manually add the following to the desired OpenCode configuration. Replace /absolute/path/to/userinput-mcp with the actual absolute path of your clone; this is an example, not a required install location:
{
"mcp": {
"servers": {
"studynext-userinput": {
"type": "local",
"command": ["node", "/absolute/path/to/userinput-mcp/dist/server.js"],
"disabled": false
}
}
}
}
This is documentation only. The independent repository intentionally does not edit global or project OpenCode config, and does not modify the project copy at tools/userinput-mcp/.