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.

Related MCP server: Forgetful

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

A
license - permissive license
-
quality - not tested
C
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

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