Skip to main content
Glama
jeswr
by jeswr

@jeswr/solid-mcp

A Model Context Protocol (MCP) server that exposes a Solid 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

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

Auth model (the seam)

The server takes an injectable authenticated fetch and a podRoot:

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:

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'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) item.

Claude Desktop config

Add to claude_desktop_config.json (mcpServers):

{
  "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

  • @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, …), 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

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 (generated by API Extractor). 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

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, which pushes a normalizing commit onto the PR branch if Dependabot's own regenerated lockfile needs it.

License

MIT © Jesse Wright

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jeswr/solid-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server