skosmos-mcp
# skosmos-mcp
A production-quality [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server that wraps the [Skosmos](https://skosmos.org/) REST API, enabling AI assistants to navigate and query SKOS vocabularies. Also includes SPARQL query capabilities for direct RDF data access.
---
## Features
- **17 MCP tools** covering vocabulary browsing, concept lookup, full-text search, label resolution, BFS traversal, and schema-guided assistance
- **4 SPARQL tools** for direct SPARQL query execution, updates, graph discovery, and query templates
- **3 MCP resources** for direct URI-based access to vocabularies and concepts
- **BFS traversal engine** with configurable depth cap, cycle detection, and duplicate elimination
- **TTL-based in-memory cache** to avoid redundant API calls
- **Retry logic** with exponential backoff for 5xx and network errors
- **AbortController timeout** on every HTTP request
- **Strict TypeScript** (strict mode, `noUncheckedIndexedAccess`, `exactOptionalPropertyTypes`)
- **Zod-validated inputs** on all tools
- **stdio transport** — reads from stdin, writes to stdout; all logging goes to stderr
- **StreamableHTTP transport** — HTTP server at `/mcp` for remote or web-based MCP clients
---
## Installation
```bash
npm install
npm run build
```
Or run directly with tsx:
```bash
npm run dev
```
### Docker / Docker Compose
Build and run the Streamable HTTP MCP server in a container:
```bash
docker compose up --build -d
```
This starts the Streamable HTTP MCP server on port `3000` and uses Docker Compose's `restart: unless-stopped` policy so it will come back up automatically after crashes. The image defaults to the Finto endpoints, runs the HTTP MCP server on `0.0.0.0:3000`, and enables alternate Skosmos/SPARQL connections by default. The container logs a warning at startup when those options are enabled because allowing other endpoints can be a security risk. The container reads the same environment variables as the local app, so copy `.env.example` to `.env` if you want to override those defaults.
#### Container Images from GitHub Container Registry
Releases publish two container image variants to [GitHub Container Registry (GHCR)](https://docs.github.com/en/packages/working-with-a-package-registry/working-with-the-container-registry):
**HTTP variant** (for remote access via HTTP):
```bash
docker pull ghcr.io/jsilvanus/skosmos-mcp:http
docker pull ghcr.io/jsilvanus/skosmos-mcp:http-latest
# Or use a specific release version:
docker pull ghcr.io/jsilvanus/skosmos-mcp:v0.2.1-http
```
**Stdio variant** (for local stdio MCP protocol):
```bash
docker pull ghcr.io/jsilvanus/skosmos-mcp:stdio
docker pull ghcr.io/jsilvanus/skosmos-mcp:stdio-latest
# Or use a specific release version:
docker pull ghcr.io/jsilvanus/skosmos-mcp:v0.2.1-stdio
```
Each release publishes both variants automatically. Choose the one that matches your use case:
- **HTTP variant**: Runs an HTTP server on port 3000, suitable for remote access or web-based MCP clients
- **Stdio variant**: Uses stdin/stdout for the MCP protocol, suitable for local integration with AI assistants or other MCP clients
---
## Configuration
Copy `.env.example` to `.env` and fill in values:
```env
SKOSMOS_BASE_URL=https://api.finto.fi # required
SKOSMOS_DEFAULT_VOCABULARY= # optional
SKOSMOS_DEFAULT_LANGUAGE=en
SKOSMOS_TIMEOUT=30000
SKOSMOS_USER_AGENT=skosmos-mcp/0.2.0
SKOSMOS_CACHE_TTL=300
SKOSMOS_MAX_TRAVERSAL_DEPTH=5
SKOSMOS_TOOL_SERVER_URL_ALLOWED=true
# SPARQL Configuration (optional)
SPARQL_ENDPOINT_URL=https://api.finto.fi/sparql
SPARQL_USERNAME=
SPARQL_PASSWORD=
SPARQL_ALLOW_OTHER_ENDPOINTS=true
```
| Variable | Default | Description |
|---|---|---|
| `SKOSMOS_BASE_URL` | *(required)* | Base URL of the Skosmos instance |
| `SKOSMOS_DEFAULT_VOCABULARY` | — | Default vocabulary id when not specified in a tool call |
| `SKOSMOS_DEFAULT_LANGUAGE` | `en` | Default language code for labels |
| `SKOSMOS_TIMEOUT` | `30000` | HTTP request timeout in milliseconds |
| `SKOSMOS_USER_AGENT` | `skosmos-mcp/0.1.0` | User-Agent header sent with API requests |
| `SKOSMOS_CACHE_TTL` | `300` | Cache entry TTL in seconds |
| `SKOSMOS_MAX_TRAVERSAL_DEPTH` | `3` | Hard cap on BFS traversal depth |
| `SKOSMOS_TOOL_SERVER_URL_ALLOWED` | `false` | When `true`, allows tools to accept optional `server_url` parameter to call a different Skosmos instance |
| `LOG_LEVEL` | `info` | Log level: debug, info, warn, error (written to stderr) |
| `MCP_HTTP_PORT` | `3000` | TCP port for the StreamableHTTP server |
| `MCP_HTTP_HOST` | `127.0.0.1` | Bind address for the StreamableHTTP server |
| `SPARQL_ENDPOINT_URL` | — | SPARQL endpoint URL (optional; enables SPARQL tools) |
| `SPARQL_USERNAME` | — | Username for SPARQL endpoint HTTP Basic auth (optional) |
| `SPARQL_PASSWORD` | — | Password for SPARQL endpoint HTTP Basic auth (optional) |
| `SPARQL_ALLOW_OTHER_ENDPOINTS` | `false` | When `true`, allows SPARQL tools to accept optional `endpoint` parameter to query a different SPARQL endpoint |
---
## MCP Tools Reference
### Vocabulary Tools
#### `list_vocabularies`
List all available vocabularies.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `lang` | string | no | Language code for labels |
#### `get_vocabulary`
Get vocabulary metadata and top concepts.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Vocabulary identifier (e.g. `"yso"`) |
| `lang` | string | no | Language code |
---
### Concept Tools
#### `get_concept`
Fetch full concept details: labels, broader, narrower, related.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `uri` | URL | yes | Concept URI |
| `vocabulary` | string | no | Vocabulary identifier (required if no default set) |
| `lang` | string | no | Language code |
#### `get_concept_label`
Get all labels for a concept URI.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `uri` | URL | yes | Concept URI |
| `vocabulary` | string | yes | Vocabulary identifier |
| `lang` | string | no | Language code |
#### `concept_path`
Get the hierarchy path from a concept to its root.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `uri` | URL | yes | Concept URI |
| `vocabulary` | string | yes | Vocabulary identifier |
| `lang` | string | no | Language code |
---
### Search Tools
#### `search_concepts`
Full-text search across one or all vocabularies.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `query` | string | yes | Search string (supports trailing `*` wildcard) |
| `vocabulary` | string | no | Limit to this vocabulary |
| `lang` | string | no | Language code |
| `maxhits` | integer | no | Max results |
| `offset` | integer | no | Pagination offset |
#### `autocomplete`
Autocomplete concept labels by prefix.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `prefix` | string | yes | Label prefix |
| `vocabulary` | string | no | Limit to this vocabulary |
| `lang` | string | no | Language code |
| `maxhits` | integer | no | Max suggestions |
#### `resolve_label`
Resolve a label text to concept URIs.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes | Label text to resolve |
| `vocabulary` | string | yes | Vocabulary identifier |
| `lang` | string | no | Language code |
---
### Labels Tool
#### `labels`
Get all labels (prefLabel, altLabel, hiddenLabel) for a concept URI.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `uri` | URL | yes | Concept URI |
| `vocabulary` | string | yes | Vocabulary identifier |
| `lang` | string | no | Language code |
---
### Traversal Tools
All traversal tools use BFS with cycle detection. Depth is capped at `Math.min(depth, SKOSMOS_MAX_TRAVERSAL_DEPTH)`.
#### `broader_concepts`
Traverse broader (parent) concepts.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `uri` | URL | yes | Starting concept URI |
| `vocabulary` | string | yes | Vocabulary identifier |
| `depth` | integer | no | Max traversal depth |
| `lang` | string | no | Language code |
#### `narrower_concepts`
Traverse narrower (child) concepts.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `uri` | URL | yes | Starting concept URI |
| `vocabulary` | string | yes | Vocabulary identifier |
| `depth` | integer | no | Max traversal depth |
| `lang` | string | no | Language code |
#### `related_concepts`
Traverse related concepts.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `uri` | URL | yes | Starting concept URI |
| `vocabulary` | string | yes | Vocabulary identifier |
| `depth` | integer | no | Max traversal depth |
| `lang` | string | no | Language code |
#### `traverse_concepts`
BFS using a mix of relationship types.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `uri` | URL | yes | Starting concept URI |
| `vocabulary` | string | yes | Vocabulary identifier |
| `relationships` | array | yes | One or more of: `"broader"`, `"narrower"`, `"related"` |
| `depth` | integer | no | Max traversal depth |
| `lang` | string | no | Language code |
---
### Assistance Tools
#### `vocabulary_schema_overview`
Summarize a vocabulary's structure with top concepts, relationship hints, and suggested tasks for AI clients.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `id` | string | yes | Vocabulary identifier |
| `lang` | string | no | Language code |
| `includeTopConcepts` | boolean | no | Whether to include a top concept preview |
| `maxTopConcepts` | integer | no | Maximum number of top concept previews |
#### `query_guidance`
Return task-oriented guidance for common SKOS vocabulary workflows such as exploration, hierarchy traversal, or label resolution.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `vocabulary` | string | yes | Vocabulary identifier |
| `task` | string | no | One of `explore`, `resolve`, `hierarchy`, `related`, `search`, or `all` |
#### `reconcile_concept`
Resolve a label to one or more candidate concepts using Skosmos lookup and search.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `text` | string | yes | Label text to resolve |
| `vocabulary` | string | yes | Vocabulary identifier |
| `lang` | string | no | Language code |
| `type` | string | no | Optional concept type filter |
| `maxhits` | integer | no | Maximum number of matches |
#### `suggest_sparql_templates`
Return SKOS-oriented SPARQL templates for exploration, hierarchy tracing, labels, and related concepts.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `vocabulary` | string | no | Optional vocabulary identifier to include in the response |
| `task` | string | no | One of `explore`, `hierarchy`, `labels`, `related`, or `all` |
### SPARQL Tools
SPARQL tools enable direct querying of RDF data. Set `SPARQL_ENDPOINT_URL` environment variable to enable these tools. Supports both SPARQL 1.1 Query and Update protocols, with optional HTTP Basic authentication.
See the [Attribution](#attribution) section for licensing details about the SPARQL implementation.
#### `execute_sparql_query`
Execute a SPARQL query (SELECT, CONSTRUCT, ASK, DESCRIBE) against the configured endpoint.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `query` | string | yes | The SPARQL query to execute |
| `endpoint` | URL | no | Optional custom SPARQL endpoint (overrides default) |
**Example Query:**
```sparql
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?concept ?label
WHERE {
?concept a skos:Concept ;
skos:prefLabel ?label .
}
LIMIT 10
```
#### `execute_sparql_update`
Execute a SPARQL update query (INSERT, DELETE, etc.) against the configured endpoint.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `update` | string | yes | The SPARQL update query to execute |
| `endpoint` | URL | no | Optional custom SPARQL endpoint (overrides default) |
**Example Update:**
```sparql
PREFIX ex: <http://example.org/>
INSERT DATA {
ex:subject1 ex:predicate1 "object1" .
}
```
#### `list_sparql_graphs`
List all available named graphs in the SPARQL endpoint.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `endpoint` | URL | no | Optional custom SPARQL endpoint (overrides default) |
**Returns:** JSON array of graph URIs.
#### `sparql_query_templates`
Get pre-built SPARQL query templates for common data exploration patterns.
| Parameter | Type | Required | Description |
|---|---|---|---|
| `category` | string | yes | Template category: `exploration`, `property-paths`, `statistics`, `validation`, `schema`, or `all` |
**Categories:**
- `exploration` — Basic data discovery and statistics
- `property-paths` — Complex graph navigation using SPARQL property paths
- `statistics` — Knowledge graph metrics and analysis
- `validation` — Data quality and consistency checks
- `schema` — Structure discovery and ontology exploration
---
## MCP Resources
| URI Pattern | Description |
|---|---|
| `skosmos://vocabularies` | JSON list of all vocabularies |
| `skosmos://{vocid}` | Vocabulary metadata for `{vocid}` |
| `skosmos://{vocid}/{encodedUri}` | Concept data (labels, broader, narrower, related) |
---
## Traversal Examples
### Get all ancestors of a concept (depth 3)
```json
{
"tool": "broader_concepts",
"args": {
"uri": "http://www.yso.fi/onto/yso/p8966",
"vocabulary": "yso",
"depth": 3,
"lang": "en"
}
}
```
Response includes `nodes` (with depth), `edges` (directed relationships), `rootUri`, and `maxDepth`.
### Mixed traversal (broader + related)
```json
{
"tool": "traverse_concepts",
"args": {
"uri": "http://www.yso.fi/onto/yso/p8966",
"vocabulary": "yso",
"relationships": ["broader", "related"],
"depth": 2
}
}
```
---
## Using Optional Server URL Parameter
All 13 MCP tools support an optional `server_url` parameter. When `SKOSMOS_TOOL_SERVER_URL_ALLOWED=true` is set in the environment, you can pass a `server_url` parameter to any tool to make it query a different Skosmos instance instead of the configured `SKOSMOS_BASE_URL`.
### Example: Query a different Skosmos instance
```json
{
"tool": "get_concept",
"args": {
"uri": "http://www.yso.fi/onto/yso/p8966",
"vocabulary": "yso",
"lang": "en",
"server_url": "https://alternative-skosmos.example.org"
}
}
```
This allows a single MCP session to interact with multiple Skosmos instances. The `server_url` parameter is:
- **Optional** on all tools
- **Ignored** unless `SKOSMOS_TOOL_SERVER_URL_ALLOWED=true` (default: `false`)
- Can be any valid URL pointing to a Skosmos instance with a compatible REST API
### Why use this feature?
- Query multiple Skosmos instances in parallel within a single session
- Test against different Skosmos servers without restarting the MCP
- Support scenarios where vocabularies are distributed across multiple instances
---
### stdio (standard MCP deployment)
```bash
SKOSMOS_BASE_URL=https://skosmos.example.org node dist/index.js
```
### StreamableHTTP
```bash
SKOSMOS_BASE_URL=https://skosmos.example.org MCP_HTTP_PORT=3000 node dist/http.js
```
The server listens on `http://<MCP_HTTP_HOST>:<MCP_HTTP_PORT>/mcp` (default: `http://127.0.0.1:3000/mcp`).
Each POST request is handled as a stateless MCP session (no session ID). The `SkosmosClient` and `CacheManager` instances are shared across requests for the lifetime of the process.
### Claude Desktop (`claude_desktop_config.json`)
```json
{
"mcpServers": {
"skosmos": {
"command": "node",
"args": ["/path/to/skosmos-mcp/dist/index.js"],
"env": {
"SKOSMOS_BASE_URL": "https://skosmos.example.org",
"SKOSMOS_DEFAULT_LANGUAGE": "en"
}
}
}
}
```
---
## Development
```bash
npm run dev # run with tsx (no build)
npm run typecheck # check types without emitting
npm run test # run tests
npm run test:watch # watch mode
npm run build # compile to dist/
npm run lint # lint src/ and tests/
```
---
## Architecture
```
MCP Client (AI Assistant)
│ stdio (JSON-RPC)
▼
McpServer (SDK)
├── 17 Tools (Zod-validated)
└── 3 Resources
│
┌────┴────┐
│ │
TraversalEngine CacheManager
(BFS + cycle (TTL, per-type)
detection)
│
SkosmosClient
(fetch + retry
+ timeout)
│
Skosmos REST API
```
### Key Design Decisions
- **No global mutable state**: config, client, cache, and traversal engine are created once in `src/index.ts` and passed via dependency injection.
- **BFS traversal**: uses a queue (not recursion) to ensure breadth-first ordering and avoid stack overflows.
- **Depth capping**: `Math.min(requestedDepth, config.maxTraversalDepth)` is applied in both the traversal engine and tool handlers.
- **Cache keys** include all relevant parameters: `vocabulary:${vocid}:${lang}`, `label:${vocab}:${uri}:${lang}`, etc.
- **All logging to stderr** — stdout is reserved exclusively for MCP JSON-RPC.
---
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Attribution
SPARQL functionality in this project is derived from [ramuzes/mcp-jena](https://github.com/ramuzes/mcp-jena) and is used under the MIT License.
TDQS
Scored across 13 tools
Most tools have distinct purposes, but 'get_concept_label' and 'labels' overlap significantly, both retrieving labels for a concept URI. Additionally, 'broader_concepts' and 'traverse_concepts' can be confused since traverse supports broader traversal. This creates some ambiguity for an agent.
Tool names mostly follow a consistent verb_noun pattern (e.g., list_vocabularies, get_concept, resolve_label). However, 'autocomplete' is a single word and 'concept_path' is noun_noun, breaking the pattern. The overall structure is clear despite these minor deviations.
With 13 tools, the server is well-scoped for SKOS vocabulary browsing. It covers listing, searching, detail retrieval, and hierarchy traversal without being overwhelming. The count is appropriate for its purpose.
The tool surface covers core SKOS operations: vocabulary listing, concept details, labels, hierarchy traversal (broader, narrower, related), and search (autocomplete, full-text, label resolution). Missing are concept creation/modification (likely out of scope) and some non-core features like concept collections, but the browsing workflow is complete.