Omeka S MCP server (Gouda Tijdmachine) — omeka-gtm MCP server
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Omeka S MCP server (Gouda Tijdmachine) — omeka-gtm MCP serversearch for items about the Gouda fire of 1438"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Omeka S MCP server (Gouda Tijdmachine)
An MCP server that lets Claude Code (or any other MCP client) read and write the Omeka S REST API of the Gouda Tijdmachine: https://www.goudatijdmachine.nl/omeka/api.
It runs locally over stdio: the MCP client starts it as a child process. There is no HTTP endpoint and nothing to deploy on Apache.
Tools
Tool | Kind | What it does |
| read | Search/list items, media, item sets, sites, site pages, vocabularies, properties, resource classes, resource templates, assets and users. Full-text search, property filters by term ( |
| read | One resource by type + ID, |
| read | A resource template with per property: term, label, required/private, allowed data types. |
| read | Find properties or resource classes by term, local name or label. |
| read | A site page by ID or site + slug, with its blocks (by index), public URL and |
| write | Create an item or item set with template, class, item sets, sites, visibility and values. |
| write | Change values ( |
| write | Attach media to an item via the |
| write | Create a site page, optionally with blocks. |
| write | Change a page's title, slug, visibility, layout and blocks (replace all, or append/insert/replace/update/delete/move by index). Always GET + modify + PUT, with an optional |
| write | Permanently delete an item, item set, media or site page. |
Write tools are only registered when an API key is configured and OMEKA_READ_ONLY is not set.
Every write is followed by a read-back; values Omeka silently dropped are returned under
warnings.
Related MCP server: zotero-mcp
Requirements
Node.js ≥ 20.6 (for
--env-file; this server runs Node 24) and npm.Network access to the Omeka S API.
An Omeka S API key (see below). Without a key the server still starts, read-only and anonymous.
Omeka user & API key requirements
An API key always belongs to an Omeka user and has exactly that user's permissions.
Creating a key: log in to the Omeka admin → Users → edit the user → tab API keys →
enter a label → Save. The key_identity and key_credential are shown once; copy
them straight into .env.
Reads use the key too. On this installation anonymous API responses are rewritten by the
RdfDispatch setup (no o: fields such as o:id, o:resource_template), and private
resources are invisible without a key. The read tools depend on those fields.
Which role do you need? (Omeka S core roles)
Role | Read (incl. private) | Create items/media | Edit/delete own resources | Edit/delete resources of others | Vocabularies, templates, sites |
Researcher | yes | – | – | – | – |
Author | yes | yes | yes | – | – |
Reviewer | yes | yes | yes | edit only | – |
Editor | yes | yes | yes | yes | – |
Site admin | yes | yes | yes | yes | sites |
Global admin | yes | yes | yes | yes | yes |
Recommendations:
For the write tools use a dedicated user with role Editor. Author is not enough: almost all existing items are owned by user 2 ("Gouda Tijdmachine Beheerder"), and authors can only edit their own resources. New items are owned by the key's user.
With the Teams module enabled, the user must also be a member of the team that owns the resources (and have a team role that allows editing).
For read-only use, a Researcher key plus
OMEKA_READ_ONLY=1is sufficient.OMEKA_READ_ONLY=1also hides the write tools when you reuse a more powerful key.A permission error is returned as
HTTP 403with a pointer to this section.
Install & build
git clone https://github.com/gouda-tijdmachine/Omeka-S-local-MCP-server.git
cd Omeka-S-local-MCP-server # on the GTM server this lives in /home/http/goudatijdmachine.nl/mcp
npm ci # or: npm install
cp .env.example .env
chmod 600 .env # the file contains the API credential
$EDITOR .env # fill in OMEKA_KEY_IDENTITY and OMEKA_KEY_CREDENTIAL
npm run build # compiles src/ to dist/
npm test # unit tests + a read-only live test (skipped without .env).env settings:
Variable | Default | Meaning |
|
| API base URL, no trailing slash |
| – | API key identity |
| – | API key credential |
|
|
|
|
| Wait before the read-back after a write (database replica lag) |
The directory is outside every Apache DocumentRoot, so .env is not reachable over the web.
Keep it that way if you move the server.
Run / try it out
The server talks JSON-RPC on stdin/stdout and waits for a client, so running it directly only shows the startup line on stderr:
npm start # = node --env-file=.env dist/server.js
npm run dev # same, from the TypeScript sources via tsx (no build needed)To click through the tools interactively, use the MCP Inspector (opens a local web UI):
npx @modelcontextprotocol/inspector node --env-file=.env dist/server.jsRegister in Claude Code
Project scope (only when Claude Code is started in /home/http/goudatijdmachine.nl or
below): create /home/http/goudatijdmachine.nl/.mcp.json:
{
"mcpServers": {
"omeka-gtm": {
"command": "node",
"args": [
"--env-file=/home/http/goudatijdmachine.nl/mcp/.env",
"/home/http/goudatijdmachine.nl/mcp/dist/server.js"
]
}
}
}Claude Code asks once to approve project MCP servers from .mcp.json.
User scope (available in every directory):
claude mcp add omeka-gtm --scope user -- \
node --env-file=/home/http/goudatijdmachine.nl/mcp/.env \
/home/http/goudatijdmachine.nl/mcp/dist/server.jsThen restart Claude Code and check with /mcp that omeka-gtm is connected and lists its
tools. The API key stays in .env; it never ends up in .mcp.json or ~/.claude.json.
After changing the code: npm run build, then reconnect the server via /mcp (or restart
Claude Code).
Claude Desktop (same machine): add the same command/args block under mcpServers in
claude_desktop_config.json.
Value notation
The write tools accept, and the read tools return, property values in a simplified notation, keyed by term:
{
"dcterms:title": [{ "value": "Markt", "lang": "nl" }],
"owl:sameAs": [{ "uri": "https://n2t.net/ark:/60537/b03ms0m" }],
"sdo:url": [{ "uri": "https://example.org/", "label": "Example" }],
"sdo:isPartOf": [{ "resource": 3076353 }],
"sdo:about": [{ "resource": 2, "type": "resource:itemset" }],
"sdo:datePublished": [{ "value": "1917-12-01", "type": "numeric:timestamp" }],
"sdo:geo": [{ "value": "52.0115,4.7105", "type": "geography:coordinates" }],
"sdo:keywords": [{ "value": "intern", "public": false }]
}No
type:resource→resource:item,uri→uri, otherwiseliteral.Any other data type (custom vocab
customvocab:<id>,numeric:*,geography:*,geometry:*, value suggest, ...) is passed astype. Useomeka_get_templateto see the allowed data types of a property.langon a URI value is stored as the URI's language (o:lang).Read output adds
titleto resource links and omitstypewhen it is the default.
Value annotations
Any value can carry an annotation: property values about that value, keyed by term, in
the same notation (Omeka S 4 value annotations, @annotation in the API). Typical use is the
source of a statement:
{
"sdo:description": [{
"value": "Vermeld in 1757", "lang": "nl",
"annotation": {
"prov:hadPrimarySource": [{ "resource": 266234 }],
"sdo:citation": [{ "uri": "https://gis.gouda.nl/geoserver/wfs", "label": "Gemeente Gouda, GIS-laag", "lang": "nl" }]
}
}]
}Annotations cannot be nested. Read tools return them the same way.
Updating, per term:
{
"sdo:keywords": { "mode": "add", "values": [{ "value": "Markt", "lang": "nl" }] },
"sdo:name": { "mode": "set", "values": [{ "value": "Grote Markt", "lang": "nl" }] },
"sdo:text": { "mode": "remove", "values": [] }
}remove with values removes only those values (matched on value + language, URI, or linked
resource ID); with an empty list it removes the term entirely.
Existing annotations are always kept by add, remove and by updates of other terms. add
skips a value that already exists, including any annotation given with it. To change the
annotation of an existing value use annotate: the value is matched the same way as for
remove, its annotation is replaced by the given one, and omitting annotation removes it:
{
"sdo:description": { "mode": "annotate", "values": [
{ "value": "Vermeld in 1757", "lang": "nl", "annotation": { "prov:hadPrimarySource": [{ "resource": 310639 }] } },
{ "value": "Tweede omschrijving", "lang": "nl" }
] }
}Site pages and blocks
Pages are addressed by ID or by site (ID or slug, e.g. data) + page slug. Blocks have no ID
in the API; they are addressed by their 0-based index in the page, as shown by
omeka_get_page.
A block in simplified notation:
{
"layout": "html",
"data": { "html": "<p>Text</p>" },
"layout_data": { "grid_column_position": "auto", "grid_column_span": "12" },
"attachments": [{ "item": 123, "media": 456, "caption": "Caption" }]
}data depends on the layout: {"html": ...} for html, {"markdown": ...} for
markdown (the module adds html and fingerprint itself), {} for pageTitle, and the
module-specific keys for geoJsonMap, sparql, browsePreview, verhaalhalen, ... Copy the
shape from an existing block of the same layout (omeka_get_page with block_index).
Editing blocks with omeka_update_page, operations run in order and each index refers to the
list as it is at that moment:
{
"id": 73,
"expect_modified": "2025-11-08T23:35:17+00:00",
"block_ops": [
{ "op": "update", "index": 2, "data": { "html": "<p>New text</p>" } },
{ "op": "append", "block": { "layout": "markdown", "data": { "markdown": "## Sources" } } },
{ "op": "move", "index": 3, "to": 1 },
{ "op": "delete", "index": 0 }
]
}update merges the given data/layout_data keys into the block and replaces its
attachments only when given; replace swaps in a whole new block; blocks (instead of
block_ops) replaces all blocks. Pass the modified value from omeka_get_page as
expect_modified: pages are sometimes edited by hand in the admin, and the update is refused
when the page changed in between. New pages are not added to the site navigation.
Design choices & Omeka pitfalls
Never PATCH. Omeka's value hydrator treats a PATCH that contains property values as a full replacement of all values: every value not included is deleted (including
owl:sameAs/ARK permalinks).omeka_update_resourcetherefore always does GET → merge → PUT of the complete representation, and the client has no PATCH method at all. Media, item sets, owner and all unmentioned terms are sent back unchanged.Read-back verification. Omeka silently drops values with an unknown or invalid data type instead of returning an error. After each write the resource is fetched again and every value that was sent but is not stored is listed under
warnings.Replica lag. Reads directly after a write can hit a database replica that is a moment behind; the read-back waits
OMEKA_READBACK_DELAY_MSfirst.Pages: PUT re-saves every block. A PATCH with
o:blockappends blocks, so pages are always updated with GET + modify + PUT of the whole page. That re-saves all blocks, also the ones you did not touch, and each block layout's save hook runs again. The corehtmllayout passes its HTML through HTMLPurifier when the global settinguse_htmlpurifieris on, which strips inline<style>and<script>. Several pages here depend on inline code (e.g. Hofstedegeldnummers, page 73; Goudse archivarissen). On this installation the setting is off (checked 2026-10-17:setting.use_htmlpurifier = "0"; a test page with a<script>block survived two saves unchanged). As a safety net the read-back compares every block, touched or not, with what was sent and reports any change underwarnings. If you ever switch the purifier on, do not edit such pages through the API.Deletes that report an error. On this installation the Comment module used to throw an HTTP 500 after an item was already deleted.
omeka_deletetherefore checks with a GET after a 5xx and reports the resource as deleted if it is gone.Terms instead of IDs. Property and class terms are resolved to IDs via a per-process cache of
/propertiesand/resource_classes. A new property or class added in Omeka is picked up after reconnecting the server.Small responses. Search results omit values unless
include_valuesis set, paging is explicit (max 100 per page), and the JSON-LD@contextis stripped.stdio only. Logging goes to stderr; stdout is reserved for MCP messages.
Project layout
src/server.ts entry point: configuration, tool registration, stdio transport
src/omeka.ts API client (key on every request, PHP-style query serialisation)
src/terms.ts term <-> ID cache for properties and resource classes
src/values.ts simplified value notation (incl. annotations), merge logic, read-back check, compact output
src/pages.ts simplified block notation, block operations, page read-back check
src/tools/read.ts read tools
src/tools/write.ts write tools
src/tools/pages.ts site page tools (read + write)
tests/ unit tests (values, annotations, blocks, query building) and a live read-only stdio testTODO
Bulk update tool (same term update on all results of a search, with dry run)
Item set membership by search (add/remove many items at once)
Site pages and page blocks
Value annotations (
@annotation) in the simplified notationAdd pages to / remove pages from the site navigation
Media file upload from a local path (multipart)
Automatic data type for
resourcelinks that point to item sets or mediaResource template validation (required properties, allowed data types) before writing
License
GPL-3.0-or-later. See LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
Read-only MCP server exposing a user ORANO library to their own AI agent.
Read-only MCP server: let AI agents read your ORANO saved-video library, tasks, and memory.
Query, browse, and automate OmegaAI workspaces from any MCP client. Streamable HTTP with OAuth 2.0.
Remote MCP server for supportsheep: run AI interviews and manage support content for your blog.
Related MCP Servers
- FlicenseBqualityDmaintenanceMulti-mode MCP server supporting both Claude Desktop (STDIO) and OpenAI (HTTP/SSE) integrations with file operations including read, write, delete, and search capabilities.3-
- FlicenseNot gradedqualityDmaintenanceRead-only MCP server for browsing, searching, and exporting a Zotero library from AI assistants.-
- FlicenseNot gradedqualityDmaintenanceIntegrates Omeka S API with Claude Desktop via MCP, enabling listing, creating items, and uploading media.1-
- AlicenseAqualityBmaintenanceLocal stdio Model Context Protocol server for OmniDimension. Drive voice agents, dispatch calls, and manage knowledge bases from Claude, Cursor, Windsurf, or any MCP-compatible client.5272 npm7MIT