Skip to content

Commit 4d542b7

Browse files
committed
v0.9.0: structured content, temporal tool, hermetic tests, MCP Registry
- search/fetch and the stats family declare outputSchema and return structuredContent mirrored into the text block (ChatGPT apps/deep research contract); row-heavy search_*/OCR tools deliberately stay text-only to avoid doubling their payloads - new get_temporal_distribution: counts per year/month across the dated subsets with the standard filters, group_by country|newspaper, and an undated_count so nothing is dropped silently - consolidate: TEXT_COLS single source for per-subset keyword columns, shared semantic-search runner (_semantic.ts), OCR-excerpt helper, limitWarning; every tool now carries a top-level title - harden: prune stale parquet shards and .partial files on cache refresh, IWAC_OFFLINE mode, timing-safe bearer compare, env int guards, instructions built per-server so semantic tools are only mentioned when actually registered - tests: unit suite (tsx + node:test) and an offline fixture MCP round-trip against synthetic parquet (make-fixtures.mjs); smoke test gains an EXPECTED pin block, manifest-sync and structuredContent assertions; new ci.yml runs the hermetic suite on push and the live smoke weekly - publish to the official MCP Registry on version tags via mcp-publisher GitHub OIDC: generated server.json (schema 2025-12-11) with mcpb fileSha256 entries and the public islam.zmo.de/mcp remote; Dockerfile carries the OCI ownership label
1 parent 8159b55 commit 4d542b7

35 files changed

Lines changed: 1826 additions & 382 deletions

.agents/skills/iwac-mcp/SKILL.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ description: |
1414

1515
# IWAC MCP Research Workflow
1616

17-
Structured methodology for academic research using the IWAC MCP server's 26 possible tools (24 core + 2 optional semantic). Adapted from ALA-compliant archival research practices. Applies to server **v0.8.0+** — all matching is accent- and case-insensitive; result objects use short English keys (`id`, `date`, `polarity`, `centrality`, `subjectivity`, `description_ai`, `url`); list/search tools return a pagination envelope (`count`, `total_matches`, `offset`, `limit`, `has_more`, `next_offset`); and enumerated filters are validated (see **Reading Results & Errors** below).
17+
Structured methodology for academic research using the IWAC MCP server's 27 possible tools (25 core + 2 optional semantic). Adapted from ALA-compliant archival research practices. Applies to server **v0.8.0+** — all matching is accent- and case-insensitive; result objects use short English keys (`id`, `date`, `polarity`, `centrality`, `subjectivity`, `description_ai`, `url`); list/search tools return a pagination envelope (`count`, `total_matches`, `offset`, `limit`, `has_more`, `next_offset`); and enumerated filters are validated (see **Reading Results & Errors** below).
1818

1919
## Prerequisites
2020

2121
Load reference files **as needed**, not all upfront:
2222

23-
1. **references/tools-by-phase.md** — all 26 possible tools with parameters, defaults, and verified filter vocabularies. Read before the first search of a session.
23+
1. **references/tools-by-phase.md** — all 27 possible tools with parameters, defaults, and verified filter vocabularies. Read before the first search of a session.
2424
2. **references/research-domains.md** — French search terms and transliteration variants by domain. Read when crafting search-term variants (Extended mode, or when a Brief search comes back thin).
2525
3. **references/biases-and-limitations.md** — collection biases, coverage gaps, sentiment caveats. Read before writing the synthesis.
2626
4. **references/capabilities-overview.md** — plain-language description of the collection and recommended ways into the data. Read when the user asks what you can do (see "Capability Questions" below).
@@ -29,7 +29,7 @@ For data schema and Omeka S API details, defer to the `iwac-data` skill.
2929

3030
## Capability Questions
3131

32-
When the user asks what you can do with IWAC ("what can you do?", "qu'est-ce que tu peux faire ?", "what's in this collection?", "how could I search this?"), do **not** launch the research workflow, present the depth choice, or enumerate the 26 tools. Read **references/capabilities-overview.md** and answer in plain language, in the user's language:
32+
When the user asks what you can do with IWAC ("what can you do?", "qu'est-ce que tu peux faire ?", "what's in this collection?", "how could I search this?"), do **not** launch the research workflow, present the depth choice, or enumerate the 27 tools. Read **references/capabilities-overview.md** and answer in plain language, in the user's language:
3333

3434
1. One short paragraph on what the collection is and covers.
3535
2. The main ways into the data (keyword, curated themes, people/organizations, semantic, sentiment, periodicals, scholarship) — described as research moves, not tool names.

.agents/skills/iwac-mcp/references/capabilities-overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ Because reports are date-stamped and location-specific, the collection also supp
4141
- **People, organisations, places** — find the canonical name among the 4,700 index records, then pull every article that mentions it.
4242
- **Conceptual (semantic) search** in any language, when keywords would miss paraphrases — "Islamic education reform" also surfaces madrasa-modernisation pieces.
4343
- **Tone and framing** — every article carries AI sentiment labels (polarity, centrality, subjectivity); compare how topics, countries or outlets frame Islam.
44+
- **Trends over time** — chart how coverage of a topic rose and fell year by year (per country or outlet), directly, without leafing through the articles themselves.
4445
- **By outlet and period** — filter by newspaper, country, date range; contrast state vs. private press, pre- vs. post-1990.
4546
- **Inside Islamic periodicals** — browse the 25 series, search tables of contents (complete for 17 smaller magazines), or pull keyword excerpts from a full issue. The same keyword-excerpt reading works on long archival documents and articles too.
4647
- **Scholarship** — connect press findings to the 864 academic references.

.agents/skills/iwac-mcp/references/tools-by-phase.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# IWAC MCP Tools by Research Phase
22

3-
26 possible tools (24 core + 2 optional semantic) organized by the workflow phase where they are most useful. Server **v0.8.0+**: **all keyword/filter matching is accent- and case-insensitive**; result rows use short English keys (`id`, `date`, `polarity`, `centrality`, `subjectivity`, `description_ai`, `url`) and omit empty fields. List/search tools return a pagination envelope — `count`, `total_matches`, `offset`, `limit` (applied), `has_more`, `next_offset`, plus `requested_limit` + `limit_warning` when you exceed a tool's max. Enumerated filters (`country`, `polarity`, `centrality`, `index_type`) are **validated**: an invalid value returns `{error, valid_values}` (`isError`) instead of a silent zero-result.
3+
27 possible tools (25 core + 2 optional semantic) organized by the workflow phase where they are most useful. Server **v0.8.0+**: **all keyword/filter matching is accent- and case-insensitive**; result rows use short English keys (`id`, `date`, `polarity`, `centrality`, `subjectivity`, `description_ai`, `url`) and omit empty fields. List/search tools return a pagination envelope — `count`, `total_matches`, `offset`, `limit` (applied), `has_more`, `next_offset`, plus `requested_limit` + `limit_warning` when you exceed a tool's max. Enumerated filters (`country`, `polarity`, `centrality`, `index_type`, and on the temporal tool `subset`, `granularity`, `group_by`) are **validated**: an invalid value returns `{error, valid_values}` (`isError`) instead of a silent zero-result. Server **v0.9.0+** adds `get_temporal_distribution` (counts per year/month — use it for any "how did coverage evolve" question instead of paging through searches).
44

55
## Cross-Collection Entry Points
66

@@ -172,6 +172,16 @@ Detailed index entry. **Raw dataset columns, French names** (Titre, Titre altern
172172

173173
## Phase 4: Triangulation Tools
174174

175+
### get_temporal_distribution *(v0.9.0+)*
176+
Counts of matching items per year (or month) — one call replaces paging through search results for any trend question. Also useful in Phase 1 to scope a topic's timeline before searching.
177+
- `subset` (optional, validated): articles (default) | publications | references | documents | audiovisual
178+
- `granularity` (optional, validated): year (default) | month — items dated only to a year keep a bare-year key even at month granularity
179+
- `keyword` (optional): ONE substring over the subset's text fields (same semantics as the subset's search tool)
180+
- `country` / `newspaper` / `subject` / `date_from` / `date_to` (optional): same semantics as the subset's search tool
181+
- `group_by` (optional, validated): country | newspaper — returns `distribution_by_group` (one map per group) instead of `distribution`
182+
- Returns `total_matches`, `dated_count`, `undated_count` (undated items are counted, never silently dropped), and the `distribution` map sorted by year
183+
- **Tip:** `get_temporal_distribution(keyword="hadj", group_by="country")` charts six decades of hajj coverage per country in a single ~1k-token call.
184+
175185
### get_sentiment_distribution
176186
Aggregated Gemini sentiment counts.
177187
- `country` (optional, exact name), `newspaper` (optional), `subject` (optional)
@@ -213,5 +223,6 @@ See `search_references` above (12 values, with counts).
213223
- Triage with `with_description=true` (limit ≤ 10) instead of calling `get_article` on everything; read full OCR only for the 2-3 finalists (Brief) / 6-8 (Extended)
214224
- A `search_articles` page of 20 ≈ 2.5k tokens; `get_article` ≈ 1-7k tokens; capped `get_publication_fulltext`~7k tokens (+ ~1.6k when the issue has a TOC)
215225
- Use stats/distribution tools for overviews before fetching individual items; when `total_matches` exceeds ~50, analyze metadata rather than reading items
226+
- For "how did coverage evolve" questions, one `get_temporal_distribution` call (~1k tokens) replaces paging through result envelopes year by year
216227
- Combine filters (country + subject/keyword + date range) to narrow before reading
217228
- For temporal filtering: articles take `YYYY-MM-DD` or `YYYY`; publications/references take years

.github/workflows/ci.yml

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
name: CI
2+
3+
# Two layers:
4+
# - test: hermetic, runs on every push/PR — typecheck, build, unit tests, and
5+
# the offline fixture MCP round-trip (synthetic parquet, IWAC_OFFLINE=1).
6+
# - live-smoke: weekly (and on demand) — the full smoke test against the real
7+
# Hugging Face dataset. Its pinned counts (smoke-test.mjs EXPECTED block) are
8+
# the dataset-drift alarm, so a red weekly run usually means the dataset
9+
# changed, not the code.
10+
11+
on:
12+
push:
13+
branches: [main]
14+
pull_request:
15+
schedule:
16+
- cron: "17 5 * * 1" # Mondays 05:17 UTC — weekly live smoke
17+
workflow_dispatch:
18+
19+
jobs:
20+
test:
21+
if: github.event_name == 'push' || github.event_name == 'pull_request'
22+
runs-on: ubuntu-latest
23+
defaults:
24+
run:
25+
working-directory: mcpb
26+
steps:
27+
- name: Checkout
28+
uses: actions/checkout@v7
29+
30+
- name: Set up Node
31+
uses: actions/setup-node@v6
32+
with:
33+
node-version: 24
34+
cache: npm
35+
cache-dependency-path: mcpb/package-lock.json
36+
37+
- name: Install dependencies
38+
run: npm ci
39+
40+
- name: Typecheck
41+
run: npm run typecheck
42+
43+
- name: Build server bundle
44+
run: npm run build
45+
46+
- name: Unit tests
47+
run: npm run test:unit
48+
49+
- name: Fixture MCP round-trip (offline)
50+
run: npm run test:fixture
51+
52+
live-smoke:
53+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
54+
runs-on: ubuntu-latest
55+
defaults:
56+
run:
57+
working-directory: mcpb
58+
steps:
59+
- name: Checkout
60+
uses: actions/checkout@v7
61+
62+
- name: Set up Node
63+
uses: actions/setup-node@v6
64+
with:
65+
node-version: 24
66+
cache: npm
67+
cache-dependency-path: mcpb/package-lock.json
68+
69+
- name: Restore HF parquet cache (~250 MB)
70+
uses: actions/cache@v6
71+
with:
72+
path: ~/.iwac-mcp/cache
73+
key: iwac-hf-parquet-${{ github.run_id }}
74+
restore-keys: |
75+
iwac-hf-parquet-
76+
77+
- name: Install dependencies
78+
run: npm ci
79+
80+
- name: Build server bundle
81+
run: npm run build
82+
83+
- name: Live smoke test (real dataset)
84+
run: node smoke-test.mjs

.github/workflows/docker-publish.yml

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ name: Publish container image to GHCR
66
#
77
# Triggers on a version tag (v*), or manually. Version tags also publish the
88
# Claude Desktop / stdio .mcpb bundles and the .agents research skill zip as
9-
# GitHub release assets.
9+
# GitHub release assets, then publish the server to the official MCP Registry
10+
# (registry.modelcontextprotocol.io) as io.github.fmadore/iwac-mcp-server.
1011

1112
on:
1213
push:
@@ -24,6 +25,7 @@ jobs:
2425
permissions:
2526
contents: write
2627
packages: write
28+
id-token: write # MCP Registry publish via GitHub OIDC (no secret needed)
2729
steps:
2830
- name: Checkout
2931
uses: actions/checkout@v4
@@ -79,3 +81,26 @@ jobs:
7981
mcpb/iwac-mcp-server-macos.mcpb \
8082
iwac-mcp-skill.zip \
8183
--clobber
84+
85+
# MCP Registry publish. server.json is generated HERE, in the same job
86+
# that packed and uploaded the .mcpb assets, so the embedded fileSha256
87+
# hashes always match the exact uploaded bytes. Registry versions are
88+
# immutable: re-running this workflow for an already-published tag fails
89+
# at this step by design (re-packed zips hash differently) — bump the
90+
# version instead of re-publishing.
91+
- name: Generate server.json
92+
if: startsWith(github.ref, 'refs/tags/v')
93+
env:
94+
TAG: ${{ github.ref_name }}
95+
run: node mcpb/scripts/make-server-json.mjs "$TAG"
96+
97+
- name: Install mcp-publisher
98+
if: startsWith(github.ref, 'refs/tags/v')
99+
run: |
100+
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
101+
102+
- name: Publish to MCP Registry
103+
if: startsWith(github.ref, 'refs/tags/v')
104+
run: |
105+
./mcp-publisher login github-oidc
106+
./mcp-publisher publish

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@ mcpb/server/
99
mcpb/*.mcpb
1010
iwac-mcp-skill.zip
1111
.duckdb-binding-stash/
12+
# Generated per-release by mcpb/scripts/make-server-json.mjs (embeds artifact
13+
# hashes) — regenerate, never hand-edit or commit.
14+
/server.json
15+
16+
# Generated synthetic test fixtures (scripts/make-fixtures.mjs)
17+
mcpb/test/fixtures/
1218

1319
# Local cache / secrets
1420
.cache/

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,18 +68,20 @@ Download `iwac-mcp-skill.zip` from the same release, then:
6868

6969
## What it gives Claude
7070

71-
26 possible read-only tools across six IWAC subsets. **24 work out of the box**;
71+
27 possible read-only tools across six IWAC subsets. **25 work out of the box**;
7272
the 2 `semantic_search_*` tools are optional and require a free Google/Gemini API
7373
key (disabled by default). All keyword and filter matching is accent- and
74-
case-insensitive.
74+
case-insensitive. The unified `search`/`fetch` pair and the stats tools also
75+
return MCP structured content (`outputSchema` + `structuredContent`), which the
76+
ChatGPT connector contract requires.
7577

7678
| Group | Tools |
7779
| ------------ | ------------------------------------------------------------------------------------------- |
7880
| Cross-subset | `search`, `fetch` |
7981
| Articles | `search_articles`, `get_article`, `semantic_search_articles` |
8082
| Sentiment | `search_by_sentiment`, `get_sentiment_distribution` |
8183
| Index | `search_index`, `get_index_entry`, `list_subjects`, `list_locations`, `list_persons` |
82-
| Stats | `get_collection_stats`, `get_newspaper_stats`, `get_country_comparison` |
84+
| Stats | `get_collection_stats`, `get_newspaper_stats`, `get_country_comparison`, `get_temporal_distribution` |
8385
| Publications | `search_publications`, `list_periodicals`, `get_publication_fulltext`, `semantic_search_publications` |
8486
| References | `search_references`, `get_reference` |
8587
| Other | `search_documents`, `get_document`, `search_audiovisual`, `list_audiovisual`, `get_audiovisual` |
@@ -115,9 +117,14 @@ npm install
115117
node scripts/install-duckdb-bindings.mjs
116118
npm run typecheck # tsc --noEmit
117119
npm run build # esbuild -> single server/index.js
118-
node smoke-test.mjs
120+
npm test # unit tests + offline fixture MCP round-trip (no network)
121+
npm run test:live # full smoke test against the real HF dataset (~250 MB)
119122
```
120123

124+
CI runs the typecheck, build, unit tests, and the offline fixture test on every
125+
push; the live smoke test runs weekly (its pinned counts are the dataset-drift
126+
alarm).
127+
121128
## Roadmap
122129

123130
See [TODO.md](TODO.md) — near-term: submit to the Anthropic extension directory,

TODO.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@
99
1010
## Distribution & Roadmap
1111

12+
- [x] **Publish to the official MCP Registry** — automated since v0.9.0: the
13+
tag workflow generates `server.json` (`mcpb/scripts/make-server-json.mjs`)
14+
and publishes `io.github.fmadore/iwac-mcp-server` (2 `.mcpb` packages + the
15+
`islam.zmo.de/mcp` remote) via `mcp-publisher` GitHub OIDC. Versions are
16+
immutable — fixing an entry means bumping and re-tagging. The Anthropic
17+
directory below is a separate, manual submission.
18+
1219
- [ ] **Submit to the Anthropic extension directory**
1320
Fill out the interest form:
1421
<https://docs.google.com/forms/d/e/1FAIpQLScHtjkiCNjpqnWtFLIQStChXlvVcvX8NPXkMfjtYPDPymgang/viewform>
@@ -39,6 +46,16 @@
3946
- [ ] **Add `screenshots/`** showing a research query in Claude Desktop — the
4047
directory listing surfaces these.
4148

49+
- [ ] **Migrate to MCP TypeScript SDK v2** (after its stable release alongside
50+
the 2026-07-28 spec — verified July 2026: `2.0.0-beta.2` current, v1.x is
51+
maintenance-only). Breaking for this server: scoped packages
52+
(`@modelcontextprotocol/server`), raw Zod shapes replaced by Standard Schema
53+
objects (`z.object(...)`), `serveStdio()` / `createMcpHandler()` replacing the
54+
v1 stdio + StreamableHTTP transports (stateless core). Start with the official
55+
codemod (`npx @modelcontextprotocol/codemod@beta v1-to-v2 .`), then re-run the
56+
fixture + live test suites. Re-check the final 2026-07-28 changelog first —
57+
details were RC-stage when noted.
58+
4259
## Data Enrichment (Track 2 — runs in the IWAC-Hugging-Face pipeline, not here)
4360

4461
> Governing rule: all AI enrichment is precomputed offline as HF columns and

docs/connecting.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ Download `iwac-mcp-skill.zip` from the same release, then in Claude Desktop open
6464

6565
### Optional: semantic search
6666

67-
Twenty tools work out of the box. Two extra tools find articles and publications
68-
by *meaning* rather than keywords; they need a free Google/Gemini API key and are
69-
**off by default**. To enable them, turn on **Enable semantic search** in the
67+
Twenty-five tools work out of the box. Two extra tools find articles and
68+
publications by *meaning* rather than keywords; they need a free Google/Gemini
69+
API key and are **off by default**. To enable them, turn on **Enable semantic search** in the
7070
extension's settings and paste a key from
7171
[Google AI Studio](https://aistudio.google.com/apikey). Most users don't need this.
7272

mcpb/.mcpbignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ tsconfig.json
55
*.ts
66
*.map
77

8-
# Local test scripts
8+
# Local test scripts & fixtures
99
smoke-test.mjs
1010
probe-cols.mjs
1111
download-all.mjs
12+
test/
1213

1314
# Built bundles (never pack a bundle inside another bundle)
1415
*.mcpb

0 commit comments

Comments
 (0)