Skip to content
Conduit

mcp server

ics-mcp

MCP server that lets AI assistants find, read, write and share ICS documents with your own permissions

inprojectspl/ics-mcpcommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "ics-mcp": {
      "command": "uvx",
      "args": [
        "ics-mcp"
      ]
    }
  }
}

same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12

Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs

README

synced Oct 25, 2026

ics-mcp

Connect your AI assistant to ICS, with your own permissions

An MCP server that lets Claude, Cursor and any other MCP client read, write, version and share documents in ICS (Internal Content Sharing), a self-hosted platform for sharing team documents.

License: MIT Python 3.12+ MCP 2025-11-25 FastMCP 3 53 tools

User guide · Quick start · Configuration · Tools · HTTP mode · ChatGPT · Security · Development

You: List my pinned notes in the Architecture project, show the latest diff of adr-007 and share it with an SSO link that expires on Friday.

Assistant: calls ics_assets_list, ics_asset_diff and ics_share_create_link, then answers with the diff and the new link.

Highlights

  • Acts as you, never more. Every tool call is a request to the ICS user-level API with the signed-in user's OAuth token. No shared service account and no administration tools; an administrator's token still carries that administrator's access.
  • 53 tools with full MCP annotations for discovery, reading, Markdown and HTML writes, PDF and Word documents, versions, sharing, the relation graph and task context.
  • Safe destructive operations. Deletes and overwrites go through a confirmation gate: interactive elicitation, scripted yolo (only when the operator allows it) or dry_run.
  • Two transports. stdio for a local assistant with sign-in through your browser, and Streamable HTTP for remote clients such as claude.ai connectors.
  • Token efficient. Compact responses cut to a size budget, outlines before full reads, section reads by heading, and a single call that gathers the documents of a task.
  • Standards first. OAuth 2.1 with PKCE S256, RFC 8707 resource indicators, RFC 9728 protected resource metadata and RS256 JWTs verified against the ICS JWKS.

Documentation

The user guide explains, for people who use ICS through an assistant, how to connect a client and sign in, what to ask for everyday tasks, how confirmations work and how to fix common errors. This README is the reference for tools and configuration.

For ChatGPT, follow Connect ChatGPT to ICS: it covers remote HTTP, OAuth discovery, Google Workspace sign-in, permission checks and the current file-upload limits.

Quick start

You need a running ICS with ICS_MCP_RESOURCE_URI set (ICS then registers the public clients ics-mcp-stdio and ics-mcp-http) and uv.

1. Add the server to your MCP client

Claude Desktop (claude_desktop_config.json), Claude Code (.mcp.json or claude mcp add) and Cursor (~/.cursor/mcp.json) all accept this shape:

{
  "mcpServers": {
    "ics": {
      "command": "uvx",
      "args": ["--from", "git+https://github.com/inprojectspl/ics-mcp", "ics-mcp"],
      "env": {
        "ICS_BASE_URL": "https://ics.example.com",
        "ICS_MCP_RESOURCE_URI": "https://ics.example.com/mcp"
      }
    }
  }
}

Pin a tag or a commit for reproducible setups, for example git+https://github.com/inprojectspl/ics-mcp@<tag>. Restart the client after editing its configuration.

2. Ask your assistant about ICS

The first tool call opens your browser at the ICS sign-in and consent screen. After you approve, the tokens are stored in the OS keyring and the call continues. That's it.

From a local checkout

git clone https://github.com/inprojectspl/ics-mcp && cd ics-mcp
uv sync
uv run ics-mcp

In a client configuration use "command": "uv" with "args": ["--directory", "/path/to/ics-mcp", "run", "ics-mcp"].

PyPI is planned but not published yet.

Configuration

Every setting is an environment variable, see ics_mcp/config.py and .env.example.

VariableDefaultMeaning
ICS_BASE_URLhttp://ics.localhostAddress the server uses to reach ICS. Can be an internal address in HTTP deployments; JWKS is fetched from here.
ICS_MCP_RESOURCE_URIhttp://ics.localhost/mcpCanonical 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.
ICS_MCP_OAUTH_CLIENT_IDics-mcp-stdioPublic PKCE client registered in ICS.
ICS_MCP_OAUTH_ISSUERsame as ICS_BASE_URLPublic ICS URL, the iss of its tokens. Set it when ICS_BASE_URL is internal.
ICS_MCP_TRANSPORTstdiostdio or http.
ICS_MCP_HTTP_HOST127.0.0.1HTTP bind address.
ICS_MCP_HTTP_PORT8765HTTP port.
ICS_MCP_STATELESSfalseStateless HTTP for several replicas, see HTTP mode.
ICS_MCP_EXTRA_ORIGINSemptyComma-separated browser origins allowed besides the server's own.
ICS_MCP_KEYRING_SERVICEics-mcpKeyring service name. Use different names to keep several token profiles.
ICS_MCP_TOKEN_FALLBACK_PATHplatform data dir, credentials.json (credentials-<service>.json for another keyring service)Token file used when no OS keyring is available.
ICS_MCP_DEFAULT_CONFIRMinteractiveInitial confirm mode: interactive, yolo or dry_run (ask is a legacy alias of interactive).
ICS_MCP_ALLOW_YOLOfalseWhether the model may switch its own session to yolo.
ICS_MCP_LOG_LEVELINFODEBUG, INFO, WARNING, ...
ICS_MCP_MAX_UPLOAD_BYTES104857600 (100 MiB)Upload size cap, checked before files are read into memory.
ICS_MCP_MAX_UPLOAD_FILES500Maximum number of files in one upload.
ICS_MCP_RESPONSE_MAX_CHARS8000Response size budget for text payloads.
ICS_MCP_HTTP_TIMEOUT_READ30Read timeout for ICS requests, in seconds.
ICS_MCP_HTTP_TIMEOUT_WRITE300Write timeout for ICS requests (uploads), in seconds.

Signing in (stdio)

sequenceDiagram
  participant A as Assistant
  participant M as ics-mcp
  participant B as Browser
  participant I as ICS
  A->>M: first tool call
  M->>B: open /api/auth/oauth/authorize (PKCE, resource)
  B->>I: sign in and approve
  I->>M: redirect to 127.0.0.1 callback with a code
  M->>I: exchange the code for tokens
  M->>M: store tokens in the OS keyring
  M->>I: the original request, as you
  M-->>A: result
  • The loopback callback binds to 127.0.0.1 on a random port and checks Host and state. The flow times out after 5 minutes.
  • Access tokens are RS256 JWTs; refresh tokens rotate. On a 401 the server refreshes once and retries without the assistant noticing. Reusing a rotated refresh token revokes the whole token family in ICS.
  • Tokens are stored in the macOS Keychain, Windows Credential Manager or libsecret under service ics-mcp, account default.
  • Headless environments (no $DISPLAY or $BROWSER, and CI or ICS_MCP_FORCE_HEADLESS set): no browser opens and no loopback callback runs, so the tool returns auth_required with the authorization_url and the sign-in cannot finish there. Sign in on a machine with a browser, or run the server in HTTP mode.
  • Signing out: delete the keyring entry (see Troubleshooting). The refresh token on the ICS side expires on its own (30 days by default).

HTTP mode

With ICS_MCP_TRANSPORT=http the server speaks Streamable HTTP on /mcp, for remote clients such as claude.ai connectors:

ICS_MCP_TRANSPORT=http \
ICS_BASE_URL=http://backend:8000 \
ICS_MCP_OAUTH_ISSUER=https://ics.example.com \
ICS_MCP_RESOURCE_URI=https://mcp.ics.example.com/mcp \
ICS_MCP_HTTP_HOST=0.0.0.0 \
uvx --from git+https://github.com/inprojectspl/ics-mcp ics-mcp
  • A request without a valid bearer token gets 401 with a WWW-Authenticate header pointing at the RFC 9728 metadata, which names ICS as the authorization server, so clients run the OAuth flow against ICS on their own.
  • Tokens are verified against the ICS JWKS (signature, issuer, audience, expiry), and every call to ICS carries the caller's own token. The server keeps no shared token store.
  • content_path arguments are refused, and a request whose Origin is neither the server's own nor in ICS_MCP_EXTRA_ORIGINS gets 403.
  • GET /health returns {status, version, protocol_version} without calling ICS.
  • Sessions and their confirm mode live in process memory. For several replicas set ICS_MCP_STATELESS=true; the confirm mode then falls back to ICS_MCP_DEFAULT_CONFIRM.

Run it behind a TLS-terminating reverse proxy that does not buffer text/event-stream. deploy/ has a Dockerfile, a compose snippet, an example Caddyfile and a deployment guide.

For a custom ChatGPT connection, see the ChatGPT guide. Before connecting, check the public discovery routes without signing in or calling any tools:

uv run python scripts/check_remote.py \
  --mcp-url https://mcp.ics.example.com/mcp \
  --issuer https://ics.example.com

Tools

53 tools. Every one declares all four MCP annotations (readOnlyHint, destructiveHint, idempotentHint, openWorldHint) and returns structured content. Documents are addressed by slug_or_id (a slug or a UUID), except ics_asset_undelete, which needs the UUID. ics_help returns longer guides to the assistant.

GroupTools
Session and helpics_help, ics_whoami, ics_session_set_mode, ics_session_status
Discoveryics_assets_list, ics_assets_search, ics_asset_meta, ics_asset_list_files, ics_asset_outline, ics_projects_list, ics_shares_list
Readics_asset_read, ics_asset_ocr, ics_asset_read_rendered, ics_asset_versions, ics_asset_diff, ics_share_resolve
Write Markdownics_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
HTML bundlesics_asset_create_html, ics_asset_html_replace_bundle, ics_asset_html_security_review
PDF and Wordics_asset_create_document, ics_asset_replace_document
Metadataics_doc_types, ics_asset_set_meta
Relation graphics_asset_relations, ics_relation_add, ics_relation_update, ics_relation_remove, ics_graph_neighbourhood, ics_asset_rescan_relations
Tasksics_task_context, ics_task_link, ics_task_unlink, ics_asset_external_refs, ics_project_links, ics_project_links_set
Versionsics_asset_new_version, ics_asset_restore_version
Sharingics_share_create_link, ics_share_grant_user, ics_share_revoke

Tools that take file content accept inline content or content_path, a local path the server reads from disk (stdio only), which keeps large files out of the model's context.

Session and help

ToolPurpose
ics_help(topic?)Guides: overview, workflows, tools, tool-selection, confirm, auth, graph, task-context.
ics_whoami()The signed-in user, scope and client.
ics_session_set_mode(mode)Confirm mode for this session: interactive, yolo, dry_run.
ics_session_status()Current mode, server and protocol versions.

Discovery

ToolPurpose
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.
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.
ics_asset_meta(slug_or_id)Metadata only, with the full summary.
ics_asset_list_files(slug_or_id, version?)Files of a collection or bundle, optionally of an older version.
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.
ics_projects_list()Projects visible to the user.
ics_shares_list(slug_or_id)Share links and user grants of a document.

Read

ToolPurpose
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.
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.
ics_asset_read_rendered(slug_or_id, version?)Rendered HTML. Expensive; prefer ics_asset_read.
ics_asset_versions(slug_or_id)Version history with authors.
ics_asset_diff(slug_or_id, v1, v2, path?)Diff between two versions.
ics_share_resolve(share_link, password?, path?, lines?)Read a document from a share link or token.

Write: Markdown

ToolPurpose
ics_asset_create_md(title, content? | content_path?, share_mode?, project_id?, tags?, doc_type?, summary?)New single Markdown document.
ics_asset_create_collection(title, files, entry_path?, share_mode?, project_id?, tags?, doc_type?, summary?)New Markdown collection. Reports progress.
ics_asset_rename(slug_or_id, title)Change the title; the slug stays.
ics_asset_update_content(slug_or_id, content? | content_path?, path?, confirm?)Replace a file's content as a new version. Destructive.
ics_asset_add_file(slug_or_id, path, content? | content_path?)Add a file to a collection.
ics_asset_create_empty_file(slug_or_id, path, change_note?)Add an empty file to a collection.
ics_asset_move_file(slug_or_id, from_path, to_path, confirm?)Move or rename a file in a collection. Destructive.
ics_asset_delete_file(slug_or_id, path, confirm?)Delete a file from a collection. Destructive.
ics_asset_delete(slug_or_id, confirm?)Soft-delete a document. Destructive.
ics_asset_undelete(asset_id)Restore a soft-deleted document (UUID only). Share links are not restored.
ics_asset_pin(slug_or_id, pinned?)Pin, or with pinned=false unpin.
ics_asset_set_tags(slug_or_id, tags)Replace the tag list.

Write: HTML bundles

ToolPurpose
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.
ics_asset_html_replace_bundle(slug_or_id, entry_path, files, confirm?)Replace the whole bundle as a new version. Destructive.
ics_asset_html_security_review(slug_or_id)Security scan status and findings.

Write: PDF and Word/ODF documents

ToolPurpose
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.
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.

Document metadata

ToolPurpose
ics_doc_types()Document types for doc_type: key, label, agent hint, default review interval.
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.

Relation graph

ToolPurpose
ics_asset_relations(slug_or_id, types?, direction?, detail?)Relations as compact [from, type, to, {id, ...}] edges plus the other ends by slug.
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.
ics_relation_update(relation_id, note?, refresh_target_version?)Change the note, or mark a changed target as checked.
ics_relation_remove(relation_id, confirm?)Delete a relation. Destructive.
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.
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.

Tasks

ToolPurpose
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.
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.
ics_task_unlink(ref_id, confirm?)Delete a task link. Destructive.
ics_asset_external_refs(slug_or_id)Tasks a document is linked to.
ics_project_links(project_slug)External projects mapped to an ICS project, and its key documents.
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.

Versions

ToolPurpose
ics_asset_new_version(slug_or_id, files, entry_path?, change_note?, confirm?)New version replacing the whole file set. Destructive.
ics_asset_restore_version(slug_or_id, version, confirm?)Make an older version current again. Destructive; history is kept.

Sharing

ToolPurpose
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.
ics_share_grant_user(slug_or_id, email, role)Grant a user editor or viewer access.
ics_share_revoke(share_id? | slug_or_id + user_id, confirm?)Disable a share link or revoke a user grant permanently. Destructive.

Confirmations and destructive operations

Operations that delete or replace content, add or remove relations, unlink tasks, replace project links, create a public share link or create a document with share_mode="public" go through a confirmation gate. The session mode decides what happens:

ModeBehaviour
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.
yoloNo 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.
dry_runDestructive tools return a preview of what they would do and send nothing to ICS.

The per-call confirm argument always wins: confirm=true runs without asking, confirm=false refuses in every mode, and an omitted confirm follows the session mode.

Resources and prompts

ResourceContent
ics://indexThe first page of the user's documents.
ics://asset/{slug}Metadata plus content (single Markdown) or outline (collection, bundle).
ics://asset/{slug}/outlineThe outline only.
ics://asset/{slug}/file/{path}One file of a collection as text.
ics://asset/{slug}@v{version}An archived version: its content or its file list.
ics://asset/{slug}@v{version}/file/{path}One file of an archived version as text.
ics://project/{slug}Project metadata and its documents.
PromptWorkflow
share_with_team(asset_slug, mode, password_hint?)Create a share link and suggest how to send it.
summarize_collection(slug, max_files?)Outline, pick files, read them, summarize.
migrate_to_collection(source_slug, new_title, first_filename)Turn a single Markdown document into a collection.

Rate limits and retries

ics-mcp assumes no limits of its own and reacts to what ICS answers:

  • 429 is retried up to 3 times, waiting as long as Retry-After says (at most 30 s per wait, 1 s without the header), for every method, since ICS did not process the request. When the retries run out the tool returns rate_limited.
  • 502, 503 and 504 are retried with growing delays only for GET, HEAD and OPTIONS. Writes that fail this way go back to the assistant at once, because ICS may have applied them.

For bulk work, page through ics_assets_list with cursor and filter there, and replace many files in one ics_asset_new_version call instead of many ics_asset_update_content calls.

Security model

  • Scope of access: exactly what the signed-in user can do in ICS (own documents, documents shared with them, their projects), and nothing more. Tokens are never returned to the assistant; ics_whoami shows only the client, scope and audience.
  • Audience binding (RFC 8707): tokens are issued for this server's resource URI; in HTTP mode tokens with another aud are rejected.
  • No token passthrough: tokens go only to ICS, which is both the authorization and the resource server.
  • Token storage: the OS keyring first. The fallback file is created with mode 0600 and obfuscated with Fernet using a key derived from the host and user ID, which protects against accidental reading, not against code running as the same user.
  • HTTP transport: Origin validation as the MCP spec requires, and content_path disabled.
  • Logs: always on stderr (stdout carries the stdio protocol) and never with tokens; a test enforces it.

Please report vulnerabilities privately as described in SECURITY.md.

Troubleshooting

The refresh failed or the tokens were revoked. Delete the keyring entry and call a tool again:

# macOS
security delete-generic-password -s ics-mcp -a default
# Linux (libsecret)
secret-tool clear service ics-mcp username default
# Windows
cmdkey /delete:ics-mcp

Check that ICS_MCP_RESOURCE_URI is identical on the ICS and the MCP side, that ICS_MCP_OAUTH_ISSUER matches the iss of ICS tokens, and that the client supports dynamic client registration (or use a client pre-registered in ICS).

A 403 means the request did not come from 127.0.0.1 or had a wrong state, usually because a proxy or VPN intercepts loopback traffic; exclude 127.0.0.1 from it. If ICS is unreachable, check ICS_BASE_URL (with the scheme), that ICS is up, and firewalls or DNS.

Run with ICS_MCP_LOG_LEVEL=DEBUG and read the server's stderr (in Claude Desktop: Settings, Developer, Open MCP Logs). The MCP Inspector lists the tools and lets you call them:

npx @modelcontextprotocol/inspector \
  -e ICS_BASE_URL=https://ics.example.com \
  -e ICS_MCP_RESOURCE_URI=https://ics.example.com/mcp \
  uvx --from git+https://github.com/inprojectspl/ics-mcp ics-mcp

Development

git clone https://github.com/inprojectspl/ics-mcp && cd ics-mcp
uv sync --extra dev
uv run ruff check . && uv run ruff format --check .
uv run mypy --strict ics_mcp
uv run pytest --cov=ics_mcp --cov-fail-under=85

The tests mock ICS with respx and use an in-memory keyring, so they need neither a running ICS nor network access. See CONTRIBUTING.md for the workflow and conventions, CHANGELOG.md for releases and AGENTS.md for notes aimed at AI coding agents.

License

ics-mcp is released under the MIT License. The inProjects.ai name and logo are not covered by it, see NOTICE.

↑↓ navigate openesc close