{"id":"ce247694-282a-4cc4-b953-e5f506dcae69","slug":"ics-mcp","kind":"mcp","name":"ics-mcp","summary":"MCP server that lets AI assistants find, read, write and share ICS documents with your own permissions","owner":"inprojectspl","repoUrl":"https://github.com/inprojectspl/ics-mcp","license":"MIT","licenseTier":"permissive","trust":"community","stars":1,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-17T12:19:15.000Z","addedAt":"2026-10-21T18:20:00.330Z","logoUrl":"https://avatars.githubusercontent.com/u/53557516?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["cloud-infra"],"tags":[],"runtime":"python","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-25T21:00:10.640Z","sample":false,"url":"https://conduits.dev/tools/ics-mcp","language":"Python","transports":["stdio"],"install":{"pypi":"ics-mcp"},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"<div align=\"center\">\n\n<a href=\"https://github.com/inprojectspl/ics\">\n  <picture>\n    <source media=\"(prefers-color-scheme: dark)\" srcset=\"docs/images/logo-lockup-white.svg\">\n    <img src=\"docs/images/logo-lockup.svg\" alt=\"ICS\" height=\"64\">\n  </picture>\n</a>\n\n## ics-mcp\n\n### Connect your AI assistant to ICS, with your own permissions\n\nAn [MCP](https://modelcontextprotocol.io) server that lets Claude, Cursor and any other MCP\nclient read, write, version and share documents in\n[ICS (Internal Content Sharing)](https://github.com/inprojectspl/ics), a self-hosted platform for\nsharing team documents.\n\n[![License: MIT](https://img.shields.io/badge/license-MIT-2563eb?style=flat-square)](./LICENSE)\n[![Python 3.12+](https://img.shields.io/badge/python-3.12%2B-3776ab?style=flat-square&logo=python&logoColor=white)](./pyproject.toml)\n[![MCP 2025-11-25](https://img.shields.io/badge/MCP-2025--11--25-ff5500?style=flat-square)](https://modelcontextprotocol.io/specification/2025-11-25)\n[![FastMCP 3](https://img.shields.io/badge/FastMCP-3.x-111827?style=flat-square)](https://gofastmcp.com)\n[![53 tools](https://img.shields.io/badge/tools-53-059669?style=flat-square)](#tools)\n\n[User guide](https://inprojectspl.github.io/ics-mcp/) ·\n[Quick start](#quick-start) ·\n[Configuration](#configuration) ·\n[Tools](#tools) ·\n[HTTP mode](#http-mode) ·\n[ChatGPT](docs/guide/chatgpt.md) ·\n[Security](#security-model) ·\n[Development](#development)\n\n</div>\n\n<br>\n\n> **You:** List my pinned notes in the Architecture project, show the latest diff of adr-007\n> and share it with an SSO link that expires on Friday.\n>\n> **Assistant:** *calls `ics_assets_list`, `ics_asset_diff` and `ics_share_create_link`, then\n> answers with the diff and the new link.*\n\n## Highlights\n\n- **Acts as you, never more.** Every tool call is a request to the ICS user-level API with the\n  signed-in user's OAuth token. No shared service account and no administration tools; an\n  administrator's token still carries that administrator's access.\n- **53 tools with full MCP annotations** for discovery, reading, Markdown and HTML writes, PDF and\n  Word documents, versions, sharing, the relation graph and task context.\n- **Safe destructive operations.** Deletes and overwrites go through a confirmation gate:\n  interactive elicitation, scripted `yolo` (only when the operator allows it) or `dry_run`.\n- **Two transports.** `stdio` for a local assistant with sign-in through your browser, and\n  Streamable HTTP for remote clients such as claude.ai connectors.\n- **Token efficient.** Compact responses cut to a size budget, outlines before full reads,\n  section reads by heading, and a single call that gathers the documents of a task.\n- **Standards first.** OAuth 2.1 with PKCE S256, RFC 8707 resource indicators, RFC 9728\n  protected resource metadata and RS256 JWTs verified against the ICS JWKS.\n\n## Documentation\n\nThe [user guide](https://inprojectspl.github.io/ics-mcp/) explains, for people who use ICS through an assistant,\nhow to connect a client and sign in, what to ask for everyday tasks, how confirmations work and\nhow to fix common errors. This README is the reference for tools and configuration.\n\nFor ChatGPT, follow [Connect ChatGPT to ICS](docs/guide/chatgpt.md): it covers remote HTTP,\nOAuth discovery, Google Workspace sign-in, permission checks and the current file-upload limits.\n\n## Quick start\n\nYou need a running [ICS](https://github.com/inprojectspl/ics) with `ICS_MCP_RESOURCE_URI` set (ICS\nthen registers the public clients `ics-mcp-stdio` and `ics-mcp-http`) and\n[uv](https://docs.astral.sh/uv/).\n\n### 1. Add the server to your MCP client\n\nClaude Desktop (`claude_desktop_config.json`), Claude Code (`.mcp.json` or `claude mcp add`)\nand Cursor (`~/.cursor/mcp.json`) all accept this shape:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"ics\": {\n      \"command\": \"uvx\",\n      \"args\": [\"--from\", \"git+https://github.com/inprojectspl/ics-mcp\", \"ics-mcp\"],\n      \"env\": {\n        \"ICS_BASE_URL\": \"https://ics.example.com\",\n        \"ICS_MCP_RESOURCE_URI\": \"https://ics.example.com/mcp\"\n      }\n    }\n  }\n}\n```\n\nPin a tag or a commit for reproducible setups, for example\n`git+https://github.com/inprojectspl/ics-mcp@<tag>`. Restart the client after editing its\nconfiguration.\n\n### 2. Ask your assistant about ICS\n\nThe first tool call opens your browser at the ICS sign-in and consent screen. After you approve,\nthe tokens are stored in the OS keyring and the call continues. That's it.\n\n<details>\n<summary><b>Other ways to install</b></summary>\n\n<br>\n\n**From a local checkout**\n\n```bash\ngit clone https://github.com/inprojectspl/ics-mcp && cd ics-mcp\nuv sync\nuv run ics-mcp\n```\n\nIn a client configuration use `\"command\": \"uv\"` with\n`\"args\": [\"--directory\", \"/path/to/ics-mcp\", \"run\", \"ics-mcp\"]`.\n\n**PyPI** is planned but not published yet.\n\n</details>\n\n## Configuration\n\nEvery setting is an environment variable, see [`ics_mcp/config.py`](./ics_mcp/config.py) and\n[`.env.example`](./.env.example).\n\n<details>\n<summary><b>All variables</b></summary>\n\n<br>\n\n| Variable | Default | Meaning |\n|---|---|---|\n| `ICS_BASE_URL` | `http://ics.localhost` | Address the server uses to reach ICS. Can be an internal address in HTTP deployments; JWKS is fetched from here. |\n| `ICS_MCP_RESOURCE_URI` | `http://ics.localhost/mcp` | Canonical URI of this MCP server. Must match `ICS_MCP_RESOURCE_URI` on the ICS side: tokens carry it as `aud` (RFC 8707). Required in HTTP mode. |\n| `ICS_MCP_OAUTH_CLIENT_ID` | `ics-mcp-stdio` | Public PKCE client registered in ICS. |\n| `ICS_MCP_OAUTH_ISSUER` | same as `ICS_BASE_URL` | Public ICS URL, the `iss` of its tokens. Set it when `ICS_BASE_URL` is internal. |\n| `ICS_MCP_TRANSPORT` | `stdio` | `stdio` or `http`. |\n| `ICS_MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind address. |\n| `ICS_MCP_HTTP_PORT` | `8765` | HTTP port. |\n| `ICS_MCP_STATELESS` | `false` | Stateless HTTP for several replicas, see [HTTP mode](#http-mode). |\n| `ICS_MCP_EXTRA_ORIGINS` | empty | Comma-separated browser origins allowed besides the server's own. |\n| `ICS_MCP_KEYRING_SERVICE` | `ics-mcp` | Keyring service name. Use different names to keep several token profiles. |\n| `ICS_MCP_TOKEN_FALLBACK_PATH` | platform data dir, `credentials.json` (`credentials-<service>.json` for another keyring service) | Token file used when no OS keyring is available. |\n| `ICS_MCP_DEFAULT_CONFIRM` | `interactive` | Initial confirm mode: `interactive`, `yolo` or `dry_run` (`ask` is a legacy alias of `interactive`). |\n| `ICS_MCP_ALLOW_YOLO` | `false` | Whether the model may switch its own session to `yolo`. |\n| `ICS_MCP_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, ... |\n| `ICS_MCP_MAX_UPLOAD_BYTES` | `104857600` (100 MiB) | Upload size cap, checked before files are read into memory. |\n| `ICS_MCP_MAX_UPLOAD_FILES` | `500` | Maximum number of files in one upload. |\n| `ICS_MCP_RESPONSE_MAX_CHARS` | `8000` | Response size budget for text payloads. |\n| `ICS_MCP_HTTP_TIMEOUT_READ` | `30` | Read timeout for ICS requests, in seconds. |\n| `ICS_MCP_HTTP_TIMEOUT_WRITE` | `300` | Write timeout for ICS requests (uploads), in seconds. |\n\n</details>\n\n## Signing in (stdio)\n\n```mermaid\nsequenceDiagram\n  participant A as Assistant\n  participant M as ics-mcp\n  participant B as Browser\n  participant I as ICS\n  A->>M: first tool call\n  M->>B: open /api/auth/oauth/authorize (PKCE, resource)\n  B->>I: sign in and approve\n  I->>M: redirect to 127.0.0.1 callback with a code\n  M->>I: exchange the code for tokens\n  M->>M: store tokens in the OS keyring\n  M->>I: the original request, as you\n  M-->>A: result\n```\n\n- The loopback callback binds to `127.0.0.1` on a random port and checks `Host` and `state`.\n  The flow times out after 5 minutes.\n- Access tokens are RS256 JWTs; refresh tokens rotate. On a `401` the server refreshes once and\n  retries without the assistant noticing. Reusing a rotated refresh token revokes the whole\n  token family in ICS.\n- Tokens are stored in the macOS Keychain, Windows Credential Manager or libsecret under service\n  `ics-mcp`, account `default`.\n- **Headless environments** (no `$DISPLAY` or `$BROWSER`, and `CI` or `ICS_MCP_FORCE_HEADLESS`\n  set): no browser opens and no loopback callback runs, so the tool returns `auth_required` with\n  the `authorization_url` and the sign-in cannot finish there. Sign in on a machine with a\n  browser, or run the server in [HTTP mode](#http-mode).\n- **Signing out**: delete the keyring entry (see [Troubleshooting](#troubleshooting)). The\n  refresh token on the ICS side expires on its own (30 days by default).\n\n## HTTP mode\n\nWith `ICS_MCP_TRANSPORT=http` the server speaks Streamable HTTP on `/mcp`, for remote clients\nsuch as claude.ai connectors:\n\n```bash\nICS_MCP_TRANSPORT=http \\\nICS_BASE_URL=http://backend:8000 \\\nICS_MCP_OAUTH_ISSUER=https://ics.example.com \\\nICS_MCP_RESOURCE_URI=https://mcp.ics.example.com/mcp \\\nICS_MCP_HTTP_HOST=0.0.0.0 \\\nuvx --from git+https://github.com/inprojectspl/ics-mcp ics-mcp\n```\n\n- A request without a valid bearer token gets `401` with a `WWW-Authenticate` header pointing at\n  the RFC 9728 metadata, which names ICS as the authorization server, so clients run the OAuth\n  flow against ICS on their own.\n- Tokens are verified against the ICS JWKS (signature, issuer, audience, expiry), and every call\n  to ICS carries the caller's own token. The server keeps no shared token store.\n- `content_path` arguments are refused, and a request whose `Origin` is neither the server's own\n  nor in `ICS_MCP_EXTRA_ORIGINS` gets `403`.\n- `GET /health` returns `{status, version, protocol_version}` without calling ICS.\n- Sessions and their confirm mode live in process memory. For several replicas set\n  `ICS_MCP_STATELESS=true`; the confirm mode then falls back to `ICS_MCP_DEFAULT_CONFIRM`.\n\nRun it behind a TLS-terminating reverse proxy that does not buffer `text/event-stream`.\n[`deploy/`](./deploy) has a Dockerfile, a compose snippet, an example Caddyfile and a\n[deployment guide](./deploy/README.md).\n\nFor a custom ChatGPT connection, see the [ChatGPT guide](docs/guide/chatgpt.md). Before connecting,\ncheck the public discovery routes without signing in or calling any tools:\n\n```bash\nuv run python scripts/check_remote.py \\\n  --mcp-url https://mcp.ics.example.com/mcp \\\n  --issuer https://ics.example.com\n```\n\n## Tools\n\n53 tools. Every one declares all four MCP annotations (`readOnlyHint`, `destructiveHint`,\n`idempotentHint`, `openWorldHint`) and returns structured content. Documents are addressed by\n`slug_or_id` (a slug or a UUID), except `ics_asset_undelete`, which needs the UUID.\n`ics_help` returns longer guides to the assistant.\n\n| Group | Tools |\n|---|---|\n| Session and help | `ics_help`, `ics_whoami`, `ics_session_set_mode`, `ics_session_status` |\n| Discovery | `ics_assets_list`, `ics_assets_search`, `ics_asset_meta`, `ics_asset_list_files`, `ics_asset_outline`, `ics_projects_list`, `ics_shares_list` |\n| Read | `ics_asset_read`, `ics_asset_ocr`, `ics_asset_read_rendered`, `ics_asset_versions`, `ics_asset_diff`, `ics_share_resolve` |\n| Write Markdown | `ics_asset_create_md`, `ics_asset_create_collection`, `ics_asset_rename`, `ics_asset_update_content`, `ics_asset_add_file`, `ics_asset_create_empty_file`, `ics_asset_move_file`, `ics_asset_delete_file`, `ics_asset_delete`, `ics_asset_undelete`, `ics_asset_pin`, `ics_asset_set_tags` |\n| HTML bundles | `ics_asset_create_html`, `ics_asset_html_replace_bundle`, `ics_asset_html_security_review` |\n| PDF and Word | `ics_asset_create_document`, `ics_asset_replace_document` |\n| Metadata | `ics_doc_types`, `ics_asset_set_meta` |\n| Relation graph | `ics_asset_relations`, `ics_relation_add`, `ics_relation_update`, `ics_relation_remove`, `ics_graph_neighbourhood`, `ics_asset_rescan_relations` |\n| Tasks | `ics_task_context`, `ics_task_link`, `ics_task_unlink`, `ics_asset_external_refs`, `ics_project_links`, `ics_project_links_set` |\n| Versions | `ics_asset_new_version`, `ics_asset_restore_version` |\n| Sharing | `ics_share_create_link`, `ics_share_grant_user`, `ics_share_revoke` |\n\nTools that take file content accept inline content or `content_path`, a local path the server\nreads from disk (stdio only), which keeps large files out of the model's context.\n\n<details>\n<summary><b>Full tool reference</b></summary>\n\n<br>\n\n**Session and help**\n\n| Tool | Purpose |\n|---|---|\n| `ics_help(topic?)` | Guides: `overview`, `workflows`, `tools`, `tool-selection`, `confirm`, `auth`, `graph`, `task-context`. |\n| `ics_whoami()` | The signed-in user, scope and client. |\n| `ics_session_set_mode(mode)` | Confirm mode for this session: `interactive`, `yolo`, `dry_run`. |\n| `ics_session_status()` | Current mode, server and protocol versions. |\n\n**Discovery**\n\n| Tool | Purpose |\n|---|---|\n| `ics_assets_list(kind?, project_id?, pinned?, shared_with_me?, owned?, tag?, doc_type?, freshness?, updated_since?, touched_since?, q?, limit?, cursor?)` | Paginated list with filters, 20 per page by default and cut to the response limit. Compact items carry the summary preview, document type, freshness, maintainer, successor and estimated tokens. `updated_since` means a content change, `touched_since` any change. The cheapest first step. |\n| `ics_assets_search(q, kind?, project_id?, doc_type?, tag?, freshness?, limit?, offset?, detail?, match?)` | Full-text search in Markdown, PDF text and converted Word/ODF documents. All words by default, accent-insensitive prefixes and quoted phrases; `match=\"any\"` broadens. Continue with `next_offset`. `detail=\"full\"` returns ids, scores and highlights. |\n| `ics_asset_meta(slug_or_id)` | Metadata only, with the full summary. |\n| `ics_asset_list_files(slug_or_id, version?)` | Files of a collection or bundle, optionally of an older version. |\n| `ics_asset_outline(slug_or_id, wait?, edges?, max_level?, offset?)` | Structure (headings with `line`, `end_line` and `anchor`, files, wikilink graph) without content; outgoing relations with `edges=true`. |\n| `ics_projects_list()` | Projects visible to the user. |\n| `ics_shares_list(slug_or_id)` | Share links and user grants of a document. |\n\n**Read**\n\n| Tool | Purpose |\n|---|---|\n| `ics_asset_read(slug_or_id, path?, anchor?, lines?, encoding?, format?, max_lines?, version?)` | File content: a line range, one section by heading `anchor` (`ics://asset/<slug>#<anchor>` works too) or an older version. A PDF reads as Markdown with one `## Page N` section per page, a Word/ODF document as its converted Markdown. |\n| `ics_asset_ocr(slug_or_id, request?)` | OCR state of a scanned PDF (queued, running, done, failed); `request=True` queues OCR of the current version or retries it. |\n| `ics_asset_read_rendered(slug_or_id, version?)` | Rendered HTML. Expensive; prefer `ics_asset_read`. |\n| `ics_asset_versions(slug_or_id)` | Version history with authors. |\n| `ics_asset_diff(slug_or_id, v1, v2, path?)` | Diff between two versions. |\n| `ics_share_resolve(share_link, password?, path?, lines?)` | Read a document from a share link or token. |\n\n**Write: Markdown**\n\n| Tool | Purpose |\n|---|---|\n| `ics_asset_create_md(title, content? \\| content_path?, share_mode?, project_id?, tags?, doc_type?, summary?)` | New single Markdown document. |\n| `ics_asset_create_collection(title, files, entry_path?, share_mode?, project_id?, tags?, doc_type?, summary?)` | New Markdown collection. Reports progress. |\n| `ics_asset_rename(slug_or_id, title)` | Change the title; the slug stays. |\n| `ics_asset_update_content(slug_or_id, content? \\| content_path?, path?, confirm?)` | Replace a file's content as a new version. Destructive. |\n| `ics_asset_add_file(slug_or_id, path, content? \\| content_path?)` | Add a file to a collection. |\n| `ics_asset_create_empty_file(slug_or_id, path, change_note?)` | Add an empty file to a collection. |\n| `ics_asset_move_file(slug_or_id, from_path, to_path, confirm?)` | Move or rename a file in a collection. Destructive. |\n| `ics_asset_delete_file(slug_or_id, path, confirm?)` | Delete a file from a collection. Destructive. |\n| `ics_asset_delete(slug_or_id, confirm?)` | Soft-delete a document. Destructive. |\n| `ics_asset_undelete(asset_id)` | Restore a soft-deleted document (UUID only). Share links are not restored. |\n| `ics_asset_pin(slug_or_id, pinned?)` | Pin, or with `pinned=false` unpin. |\n| `ics_asset_set_tags(slug_or_id, tags)` | Replace the tag list. |\n\n**Write: HTML bundles**\n\n| Tool | Purpose |\n|---|---|\n| `ics_asset_create_html(title, entry_path, files, share_mode?, project_id?, tags?, doc_type?, summary?)` | Upload a bundle; each file has `path` and one of `content_text`, `content_base64` or `content_path`. ICS scans it for security issues. |\n| `ics_asset_html_replace_bundle(slug_or_id, entry_path, files, confirm?)` | Replace the whole bundle as a new version. Destructive. |\n| `ics_asset_html_security_review(slug_or_id)` | Security scan status and findings. |\n\n**Write: PDF and Word/ODF documents**\n\n| Tool | Purpose |\n|---|---|\n| `ics_asset_create_document(file_path, title?, share_mode?, project_id?, tags?, doc_type?, summary?)` | Upload a local `.pdf` (becomes `pdf_doc`, text extracted, scanned pages through OCR) or `.docx`, `.odt`, `.doc` (becomes `office_doc`, converted to Markdown). ICS scans the file for scripts, launch actions, macros and remote templates. stdio only. |\n| `ics_asset_replace_document(slug_or_id, file_path, confirm?)` | Upload a new file as the next version of a `pdf_doc` or `office_doc`. Destructive; history is kept. |\n\n**Document metadata**\n\n| Tool | Purpose |\n|---|---|\n| `ics_doc_types()` | Document types for `doc_type`: key, label, agent hint, default review interval. |\n| `ics_asset_set_meta(slug_or_id, summary?, doc_type?, maintainer_email?, review_due_at?, language?, properties?)` | Change metadata without a new version; `\"\"` clears a field. |\n\n**Relation graph**\n\n| Tool | Purpose |\n|---|---|\n| `ics_asset_relations(slug_or_id, types?, direction?, detail?)` | Relations as compact `[from, type, to, {id, ...}]` edges plus the other ends by slug. |\n| `ics_relation_add(source, target, relation_type, target_path?, target_anchor?, note?, confirm?)` | Record a typed relation: `derived_from`, `supersedes`, `depends_on`, `part_of`, `references`, `relates_to`, `duplicates`, `contradicts`. Goes through the confirmation gate. |\n| `ics_relation_update(relation_id, note?, refresh_target_version?)` | Change the note, or mark a changed target as checked. |\n| `ics_relation_remove(relation_id, confirm?)` | Delete a relation. Destructive. |\n| `ics_graph_neighbourhood(slug_or_id, depth?, types?, direction?, max_nodes?, detail?)` | Nodes and `[from, type, to]` edges around a document (depth up to 3), with metadata to decide where to go next; `skipped` counts edges of types left out. |\n| `ics_asset_rescan_relations(slug_or_id)` | Extract links to ICS documents from the current content again and record them as `references` edges. Needs editor. |\n\n**Tasks**\n\n| Tool | Purpose |\n|---|---|\n| `ics_task_context(wp, project?, ancestors?, system?, scope?, depth?, budget_tokens?, include?)` | The documents of one task in one call: required, linked, inherited from ancestors, project key documents and graph neighbours, ordered and cut to `budget_tokens`. |\n| `ics_task_link(slug_or_id, wp, role, system?, project?, required?, path?, pinned_version?, note?, label?)` | Link a document to an OpenProject work package or a Project Standard node as `context`, `deliverable` or `reference`. |\n| `ics_task_unlink(ref_id, confirm?)` | Delete a task link. Destructive. |\n| `ics_asset_external_refs(slug_or_id)` | Tasks a document is linked to. |\n| `ics_project_links(project_slug)` | External projects mapped to an ICS project, and its key documents. |\n| `ics_project_links_set(project_slug, set_external?, key_documents?, confirm?)` | Replace them (owner). Destructive; `[]` clears a list and needs `confirm=true` even in `yolo`. |\n\n**Versions**\n\n| Tool | Purpose |\n|---|---|\n| `ics_asset_new_version(slug_or_id, files, entry_path?, change_note?, confirm?)` | New version replacing the whole file set. Destructive. |\n| `ics_asset_restore_version(slug_or_id, version, confirm?)` | Make an older version current again. Destructive; history is kept. |\n\n**Sharing**\n\n| Tool | Purpose |\n|---|---|\n| `ics_share_create_link(slug_or_id, mode, password?, allowed_emails?, expires_at?, max_views?, confirm?)` | Create a link: `org_sso`, `password`, `public` or `magic_email`. `public` needs confirmation. |\n| `ics_share_grant_user(slug_or_id, email, role)` | Grant a user `editor` or `viewer` access. |\n| `ics_share_revoke(share_id? \\| slug_or_id + user_id, confirm?)` | Disable a share link or revoke a user grant permanently. Destructive. |\n\n</details>\n\n## Confirmations and destructive operations\n\nOperations that delete or replace content, add or remove relations, unlink tasks, replace\nproject links, create a `public` share link or create a document with `share_mode=\"public\"` go\nthrough a confirmation gate. The session mode\ndecides what happens:\n\n| Mode | Behaviour |\n|---|---|\n| `interactive` (default) | The client asks the user through MCP elicitation. A client without elicitation gets `confirm_required`, with a hint to pass `confirm=true` after asking the user. |\n| `yolo` | No questions, for scripted or scheduled agents. The model can switch to `yolo` only when the operator set `ICS_MCP_ALLOW_YOLO=true` or `ICS_MCP_DEFAULT_CONFIRM=yolo`, so an injected instruction cannot turn confirmations off. |\n| `dry_run` | Destructive tools return a preview of what they would do and send nothing to ICS. |\n\nThe per-call `confirm` argument always wins: `confirm=true` runs without asking,\n`confirm=false` refuses in every mode, and an omitted `confirm` follows the session mode.\n\n## Resources and prompts\n\n| Resource | Content |\n|---|---|\n| `ics://index` | The first page of the user's documents. |\n| `ics://asset/{slug}` | Metadata plus content (single Markdown) or outline (collection, bundle). |\n| `ics://asset/{slug}/outline` | The outline only. |\n| `ics://asset/{slug}/file/{path}` | One file of a collection as text. |\n| `ics://asset/{slug}@v{version}` | An archived version: its content or its file list. |\n| `ics://asset/{slug}@v{version}/file/{path}` | One file of an archived version as text. |\n| `ics://project/{slug}` | Project metadata and its documents. |\n\n| Prompt | Workflow |\n|---|---|\n| `share_with_team(asset_slug, mode, password_hint?)` | Create a share link and suggest how to send it. |\n| `summarize_collection(slug, max_files?)` | Outline, pick files, read them, summarize. |\n| `migrate_to_collection(source_slug, new_title, first_filename)` | Turn a single Markdown document into a collection. |\n\n## Rate limits and retries\n\nics-mcp assumes no limits of its own and reacts to what ICS answers:\n\n- `429` is retried up to 3 times, waiting as long as `Retry-After` says (at most 30 s per\n  wait, 1 s without the header), for every method, since ICS did not process the request. When\n  the retries run out the tool returns `rate_limited`.\n- `502`, `503` and `504` are retried with growing delays only for `GET`, `HEAD` and `OPTIONS`.\n  Writes that fail this way go back to the assistant at once, because ICS may have applied them.\n\nFor bulk work, page through `ics_assets_list` with `cursor` and filter there, and replace many\nfiles in one `ics_asset_new_version` call instead of many `ics_asset_update_content` calls.\n\n## Security model\n\n- **Scope of access**: exactly what the signed-in user can do in ICS (own documents, documents\n  shared with them, their projects), and nothing more. Tokens are never returned to the\n  assistant; `ics_whoami` shows only the client, scope and audience.\n- **Audience binding (RFC 8707)**: tokens are issued for this server's resource URI; in HTTP\n  mode tokens with another `aud` are rejected.\n- **No token passthrough**: tokens go only to ICS, which is both the authorization and the\n  resource server.\n- **Token storage**: the OS keyring first. The fallback file is created with mode `0600` and\n  obfuscated with Fernet using a key derived from the host and user ID, which protects against\n  accidental reading, not against code running as the same user.\n- **HTTP transport**: `Origin` validation as the MCP spec requires, and `content_path` disabled.\n- **Logs**: always on stderr (stdout carries the stdio protocol) and never with tokens; a test\n  enforces it.\n\nPlease report vulnerabilities privately as described in [SECURITY.md](./SECURITY.md).\n\n## Troubleshooting\n\n<details>\n<summary><b><code>auth_required</code> although you signed in (stdio)</b></summary>\n\n<br>\n\nThe refresh failed or the tokens were revoked. Delete the keyring entry and call a tool again:\n\n```bash\n# macOS\nsecurity delete-generic-password -s ics-mcp -a default\n# Linux (libsecret)\nsecret-tool clear service ics-mcp username default\n# Windows\ncmdkey /delete:ics-mcp\n```\n\n</details>\n\n<details>\n<summary><b><code>401</code> in HTTP mode</b></summary>\n\n<br>\n\nCheck that `ICS_MCP_RESOURCE_URI` is identical on the ICS and the MCP side, that\n`ICS_MCP_OAUTH_ISSUER` matches the `iss` of ICS tokens, and that the client supports dynamic\nclient registration (or use a client pre-registered in ICS).\n\n</details>\n\n<details>\n<summary><b><code>403</code> from the loopback callback, or ICS unreachable</b></summary>\n\n<br>\n\nA `403` means the request did not come from `127.0.0.1` or had a wrong `state`, usually because\na proxy or VPN intercepts loopback traffic; exclude `127.0.0.1` from it. If ICS is unreachable,\ncheck `ICS_BASE_URL` (with the scheme), that ICS is up, and firewalls or DNS.\n\n</details>\n\n<details>\n<summary><b>Debugging and manual testing</b></summary>\n\n<br>\n\nRun with `ICS_MCP_LOG_LEVEL=DEBUG` and read the server's stderr (in Claude Desktop: Settings,\nDeveloper, Open MCP Logs). The MCP Inspector lists the tools and lets you call them:\n\n```bash\nnpx @modelcontextprotocol/inspector \\\n  -e ICS_BASE_URL=https://ics.example.com \\\n  -e ICS_MCP_RESOURCE_URI=https://ics.example.com/mcp \\\n  uvx --from git+https://github.com/inprojectspl/ics-mcp ics-mcp\n```\n\n</details>\n\n## Development\n\n```bash\ngit clone https://github.com/inprojectspl/ics-mcp && cd ics-mcp\nuv sync --extra dev\nuv run ruff check . && uv run ruff format --check .\nuv run mypy --strict ics_mcp\nuv run pytest --cov=ics_mcp --cov-fail-under=85\n```\n\nThe tests mock ICS with `respx` and use an in-memory keyring, so they need neither a running\nICS nor network access. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the workflow and\nconventions, [CHANGELOG.md](./CHANGELOG.md) for releases and [AGENTS.md](./AGENTS.md) for notes\naimed at AI coding agents.\n\n## License\n\nics-mcp is released under the [MIT License](./LICENSE). The inProjects.ai name and logo are not\ncovered by it, see [NOTICE](./NOTICE).\n\n<br>\n\n<div align=\"center\">\n  <sub>Built by</sub>\n  <br>\n  <a href=\"https://inprojects.ai\">\n    <picture>\n      <source media=\"(prefers-color-scheme: dark)\" srcset=\"docs/images/inprojects-logo-white.svg\">\n      <img src=\"docs/images/inprojects-logo.svg\" alt=\"inProjects.ai\" height=\"36\">\n    </picture>\n  </a>\n</div>\n"}