ics-mcp
by inprojectspl
README.md
<div align="center">
<a href="https://github.com/inprojectspl/ics">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/logo-lockup-white.svg">
<img src="docs/images/logo-lockup.svg" alt="ICS" height="64">
</picture>
</a>
## ics-mcp
### Connect your AI assistant to ICS, with your own permissions
An [MCP](https://modelcontextprotocol.io) server that lets Claude, Cursor and any other MCP
client read, write, version and share documents in
[ICS (Internal Content Sharing)](https://github.com/inprojectspl/ics), a self-hosted platform for
sharing team documents.
[](./LICENSE)
[](./pyproject.toml)
[](https://modelcontextprotocol.io/specification/2025-11-25)
[](https://gofastmcp.com)
[](#tools)
[User guide](https://inprojectspl.github.io/ics-mcp/) ·
[Quick start](#quick-start) ·
[Configuration](#configuration) ·
[Tools](#tools) ·
[HTTP mode](#http-mode) ·
[Security](#security-model) ·
[Development](#development)
</div>
<br>
> **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](https://inprojectspl.github.io/ics-mcp/) 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.
## Quick start
You need a running [ICS](https://github.com/inprojectspl/ics) with `ICS_MCP_RESOURCE_URI` set (ICS
then registers the public clients `ics-mcp-stdio` and `ics-mcp-http`) and
[uv](https://docs.astral.sh/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:
```jsonc
{
"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.
<details>
<summary><b>Other ways to install</b></summary>
<br>
**From a local checkout**
```bash
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.
</details>
## Configuration
Every setting is an environment variable, see [`ics_mcp/config.py`](./ics_mcp/config.py) and
[`.env.example`](./.env.example).
<details>
<summary><b>All variables</b></summary>
<br>
| Variable | Default | Meaning |
|---|---|---|
| `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. |
| `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. |
| `ICS_MCP_OAUTH_CLIENT_ID` | `ics-mcp-stdio` | Public PKCE client registered in ICS. |
| `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. |
| `ICS_MCP_TRANSPORT` | `stdio` | `stdio` or `http`. |
| `ICS_MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind address. |
| `ICS_MCP_HTTP_PORT` | `8765` | HTTP port. |
| `ICS_MCP_STATELESS` | `false` | Stateless HTTP for several replicas, see [HTTP mode](#http-mode). |
| `ICS_MCP_EXTRA_ORIGINS` | empty | Comma-separated browser origins allowed besides the server's own. |
| `ICS_MCP_KEYRING_SERVICE` | `ics-mcp` | Keyring service name. Use different names to keep several token profiles. |
| `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. |
| `ICS_MCP_DEFAULT_CONFIRM` | `interactive` | Initial confirm mode: `interactive`, `yolo` or `dry_run` (`ask` is a legacy alias of `interactive`). |
| `ICS_MCP_ALLOW_YOLO` | `false` | Whether the model may switch its own session to `yolo`. |
| `ICS_MCP_LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, ... |
| `ICS_MCP_MAX_UPLOAD_BYTES` | `104857600` (100 MiB) | Upload size cap, checked before files are read into memory. |
| `ICS_MCP_MAX_UPLOAD_FILES` | `500` | Maximum number of files in one upload. |
| `ICS_MCP_RESPONSE_MAX_CHARS` | `8000` | Response size budget for text payloads. |
| `ICS_MCP_HTTP_TIMEOUT_READ` | `30` | Read timeout for ICS requests, in seconds. |
| `ICS_MCP_HTTP_TIMEOUT_WRITE` | `300` | Write timeout for ICS requests (uploads), in seconds. |
</details>
## Signing in (stdio)
```mermaid
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](#http-mode).
- **Signing out**: delete the keyring entry (see [Troubleshooting](#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:
```bash
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/`](./deploy) has a Dockerfile, a compose snippet, an example Caddyfile and a
[deployment guide](./deploy/README.md).
## 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.
| Group | Tools |
|---|---|
| Session and help | `ics_help`, `ics_whoami`, `ics_session_set_mode`, `ics_session_status` |
| Discovery | `ics_assets_list`, `ics_assets_search`, `ics_asset_meta`, `ics_asset_list_files`, `ics_asset_outline`, `ics_projects_list`, `ics_shares_list` |
| Read | `ics_asset_read`, `ics_asset_ocr`, `ics_asset_read_rendered`, `ics_asset_versions`, `ics_asset_diff`, `ics_share_resolve` |
| 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` |
| HTML bundles | `ics_asset_create_html`, `ics_asset_html_replace_bundle`, `ics_asset_html_security_review` |
| PDF and Word | `ics_asset_create_document`, `ics_asset_replace_document` |
| Metadata | `ics_doc_types`, `ics_asset_set_meta` |
| Relation graph | `ics_asset_relations`, `ics_relation_add`, `ics_relation_update`, `ics_relation_remove`, `ics_graph_neighbourhood`, `ics_asset_rescan_relations` |
| Tasks | `ics_task_context`, `ics_task_link`, `ics_task_unlink`, `ics_asset_external_refs`, `ics_project_links`, `ics_project_links_set` |
| Versions | `ics_asset_new_version`, `ics_asset_restore_version` |
| Sharing | `ics_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.
<details>
<summary><b>Full tool reference</b></summary>
<br>
**Session and help**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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**
| Tool | Purpose |
|---|---|
| `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. |
</details>
## 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:
| Mode | Behaviour |
|---|---|
| `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. |
| `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. |
| `dry_run` | Destructive 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
| Resource | Content |
|---|---|
| `ics://index` | The first page of the user's documents. |
| `ics://asset/{slug}` | Metadata plus content (single Markdown) or outline (collection, bundle). |
| `ics://asset/{slug}/outline` | The 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. |
| Prompt | Workflow |
|---|---|
| `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](./SECURITY.md).
## Troubleshooting
<details>
<summary><b><code>auth_required</code> although you signed in (stdio)</b></summary>
<br>
The refresh failed or the tokens were revoked. Delete the keyring entry and call a tool again:
```bash
# 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
```
</details>
<details>
<summary><b><code>401</code> in HTTP mode</b></summary>
<br>
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).
</details>
<details>
<summary><b><code>403</code> from the loopback callback, or ICS unreachable</b></summary>
<br>
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.
</details>
<details>
<summary><b>Debugging and manual testing</b></summary>
<br>
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:
```bash
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
```
</details>
## Development
```bash
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](./CONTRIBUTING.md) for the workflow and
conventions, [CHANGELOG.md](./CHANGELOG.md) for releases and [AGENTS.md](./AGENTS.md) for notes
aimed at AI coding agents.
## License
ics-mcp is released under the [MIT License](./LICENSE). The inProjects.ai name and logo are not
covered by it, see [NOTICE](./NOTICE).
<br>
<div align="center">
<sub>Built by</sub>
<br>
<a href="https://inprojects.ai">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/images/inprojects-logo-white.svg">
<img src="docs/images/inprojects-logo.svg" alt="inProjects.ai" height="36">
</picture>
</a>
</div>
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessNo issues