Content
# @jeswr/solid-mcp
A [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server that
exposes a [Solid](https://solidproject.org) pod to MCP clients (Claude Desktop and
other MCP hosts) as **Resources** and **Tools**.
Pod URLs map 1:1 to MCP resources, and four pod-scope-guarded tools —
`solid_list`, `solid_read`, `solid_search`, `solid_write` — let an agent browse,
read, search, and (opt-in) write pod data over an **injectable authenticated Solid
fetch**. The server holds **no bespoke crypto**: you supply the authenticated
(Solid-OIDC / DPoP) `fetch`, so token handling stays in vetted upstream libraries.
> ⚠️ **Experimental, AI-agent-generated.** Part of the `@jeswr` Solid app suite.
> Read-only by default; review before granting write access to a real pod.
## What you get
- **Resources** — every in-pod URL is an MCP resource (the resource `uri` *is* the
pod url). Containers are returned as a JSON listing, RDF resources as Turtle, and
anything else as text or base64 bytes. A `list` callback browses the pod root.
- **Tools**
| Tool | Args | Semantics |
|---|---|---|
| `solid_list` | `{ container }` | List a container's typed children (`url`, `name`, `isContainer`, `type`, `mimeType`, `size`, `modified`). `readOnlyHint`. |
| `solid_read` | `{ url }` | Read a resource — Turtle for RDF, text or base64 otherwise. Fails closed (401/403). `readOnlyHint`. |
| `solid_search` | `{ query, scope? }` | Client-side search: best-effort Type-Index discovery + a bounded recursive container scan, matching url/name and RDF literal values. No server FTS. `readOnlyHint`. |
| `solid_write` | `{ url, content, contentType }` | PUT a resource. **Disabled unless `readOnly:false`.** `destructiveHint`. |
## Install
```sh
npm install github:jeswr/solid-mcp#main
```
The package commits a self-contained `dist/` (with `@jeswr/fetch-rdf` inlined via
esbuild), so it installs and imports directly from a GitHub branch under
`ignore-scripts=true` with **no build step**. npm publish is a deferred migration
(see [Roadmap](#roadmap-m2)).
## Auth model (the seam)
The server takes an **injectable authenticated `fetch`** and a `podRoot`:
```ts
import { createSolidMcpServer } from "@jeswr/solid-mcp";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
const server = createSolidMcpServer({
fetch: session.fetch, // an authenticated Solid-OIDC / DPoP fetch you provide
podRoot: "https://alice.pod.example/",
webId: "https://alice.pod.example/profile/card#me", // optional, enables Type-Index search
readOnly: true, // DEFAULT — set false to enable solid_write
});
await server.connect(new StdioServerTransport());
```
`SolidMcpConfig`:
```ts
interface SolidMcpConfig {
fetch: typeof fetch; // authenticated Solid fetch (server holds no credentials)
podRoot: string; // absolute http(s) container URL ending in "/"
webId?: string; // optional, for Type-Index-driven search
readOnly?: boolean; // default true (writes disabled)
}
```
### Read-only by default; writes are opt-in
`solid_write` is **disabled** unless you create the server with `readOnly: false`.
When read-only, the tool returns an `isError` result (it never throws out of the
handler), so a client gets a clear "write disabled" message rather than a crash.
### Pod-scope / SSRF guard
Every Resource read and every Tool call is confined to `podRoot`. A URL is rejected
(with a `pod-scope violation` error) unless its canonical form is *within* the pod
root — this is the SSRF / capability boundary that stops an agent from using a tool
to reach an arbitrary origin or to escape the pod root via `..` path traversal. The
check itself is [`@jeswr/guarded-fetch`](https://github.com/jeswr/guarded-fetch)'s
consolidated pod-scope guard (`assertWithinPodScope` / `createPodScopedFetch`): a
segment-boundary same-origin path-prefix check on the WHATWG-normalised, canonical
URL (so encoded and dot-segment escapes are resolved away first, and every redirect
hop is re-checked, not just the initial URL) — wrapped here so its errors keep this
package's `pod-scope violation:` message prefix.
### CLI (M1)
The `solid-mcp` bin reads configuration from the environment and connects over
stdio:
| Env var | Required | Notes |
|---|---|---|
| `SOLID_MCP_POD_ROOT` | yes | absolute http(s) container URL ending in `/` |
| `SOLID_MCP_WEBID` | no | enables Type-Index search |
| `SOLID_MCP_READONLY` | no | default `"true"`; set `"false"` to enable writes |
| `SOLID_MCP_CLIENT_ID` / `_SECRET` / `_OIDC_ISSUER` / `_TOKEN_URL` | no | reserved for M2 |
**M1 auth scope:** a bundled headless client-credentials login is **not** part of
M1. If the client-credentials env vars are present, the CLI prints a clear message
and falls back to an **unauthenticated** `globalThis.fetch` (works for public
resources; protected resources fail closed). For an authenticated session today,
import `createSolidMcpServer` programmatically and pass your own authenticated
`fetch`. Bundled headless login is a [Roadmap (M2)](#roadmap-m2) item.
#### Claude Desktop config
Add to `claude_desktop_config.json` (`mcpServers`):
```jsonc
{
"mcpServers": {
"solid": {
"command": "npx",
"args": ["solid-mcp"],
"env": {
"SOLID_MCP_POD_ROOT": "https://alice.pod.example/",
"SOLID_MCP_WEBID": "https://alice.pod.example/profile/card#me",
"SOLID_MCP_READONLY": "true"
}
}
}
}
```
## RDF discipline
The package parses RDF only via [`@jeswr/fetch-rdf`](https://github.com/jeswr/fetch-rdf)
+ [`@solid/object`](https://www.npmjs.com/package/@solid/object) (container
listings via `ContainerDataset`), and serialises with `n3.Writer`. It **never**
hand-builds or hand-parses RDF.
## Anti-silo / typed data
Typed pod data is read through the suite's shared RDF shapes (Activity Streams 2.0,
schema.org, [`@jeswr/solid-task-model`](https://github.com/jeswr/solid-task-model),
…), so an agent reads the **same shapes the suite apps write** — a task created in
solid-issues, a contact in the Pod Manager, a bookmark in a pod-app are all the same
graph. This is the integration-targets contract: agent access and app access share
one data model rather than per-app silos.
## Public API
```ts
import {
createSolidMcpServer,
type SolidMcpConfig,
// pod operations (programmatic / testing):
listContainer, readResource, readRdf, search, writeResource,
// auth helpers + scope guard:
normalizePodRoot, requirePodScopedUrl, podScopedUrlOrUndefined, writesEnabled,
// types:
type PodChild, type ReadResult, type ReadRdfResult, type SearchMatch, type SearchOptions,
} from "@jeswr/solid-mcp";
```
The authoritative, diffable snapshot of the whole public surface lives in
[`etc/solid-mcp.api.md`](./etc/solid-mcp.api.md) (generated by
[API Extractor](https://api-extractor.com/)). `npm run api:check` fails on any
drift and runs in CI; after an intended API change, regenerate + commit the
report with `npm run api:report`.
## Roadmap (M2)
- **Streamable-HTTP transport** (in addition to stdio).
- **Per-platform client configs** (Cline, Open WebUI, LibreChat MCP server,
OpenClaw, …).
- **Bundled headless client-credentials login** so the CLI can run authenticated
without a programmatic embed.
- **Deeper typed-data tools** (task / contact / bookmark / calendar shape-aware
read + write) over `@jeswr/solid-task-model` and the suite shapes.
## Development
```sh
npm run lint # biome + check:lockfile-transport
npm run typecheck # tsc --noEmit
npm test # vitest
npm run build # tsc → dist/
npm run check:dist # guard the committed dist/ against src/ drift
npm run check:lockfile-transport # guard package-lock.json against the SSH git transport (#78: npm install rewrites @jeswr github: deps to git+ssh, breaking npm ci)
npm run fix:lockfile-transport # the FIX half of the #78 guard — normalizes an SSH-rewritten lockfile back to HTTPS; run after any npm install/update, before committing
```
Any `npm install` / `npm update` re-triggers the #78 rewrite (npm recomputes every git-dependency
`resolved` URL as SSH on ANY lockfile regen, even one triggered by an unrelated bump — this is why
it recurs on Dependabot PRs here even when the bump is unrelated to `@jeswr/fetch-rdf`). A
repo-local git config `insteadOf` does **not** prevent it (it only changes what the `git` binary
does when actually invoked, not what npm writes into the lockfile). `npm run fix:lockfile-transport`
is the durable fix: run it after any install/update, then `check:lockfile-transport` (or `lint`)
passes and the lockfile stays committable over HTTPS. Dependabot PRs are additionally auto-fixed by
[`.github/workflows/normalize-dependabot-lockfile.yml`](.github/workflows/normalize-dependabot-lockfile.yml),
which pushes a normalizing commit onto the PR branch if Dependabot's own regenerated lockfile needs it.
## License
MIT © [Jesse Wright](https://github.com/jeswr)
Connection Info
You Might Also Like
everything-claude-code
Complete Claude Code configuration collection - agents, skills, hooks,...
markitdown
MarkItDown-MCP is a lightweight server for converting URIs to Markdown.
cc-switch
All-in-One Assistant for Claude Code, Codex & Gemini CLI across platforms.
servers
Model Context Protocol Servers
servers
Model Context Protocol Servers
Time
A Model Context Protocol server for time and timezone conversions.