mcp-rune 0.107.0
Be star #1 Get started
SECTION IX · GUIDE 35 OF 49
Reading
12 min
Topic
analysis · tutorial
Spec
v0.107.0
Source
09-retrieval-and-graphrag/analysis-quickstart.md
On this page12 sections

Analysis Quickstart

The hands-on entry to chapter IX. Part 2 of the Quickstart — once the bookshelf is running with its 5,000-book dataset, this guide brings up postgres+pgvector, points the analysis tools at it, and walks all nine built-in summary strategies end to end — five field-level strategies on the large dataset, then four GraphRAG-aware strategies on the graph dataset (500 books with author + genre + intentional gaps).

If you prefer the architecture first, read Retrieval & GraphRAG overview (next in the chapter) and come back here when you’re ready to run something.

You’ll spend about twenty minutes: ~3 on infrastructure, ~7 on the field-level tour, ~7 on the GraphRAG tour, ~3 on graph-aware sampling and teardown.

Prerequisites

  • Part 1 of the Quickstart running locally — specifically the bookshelf template scaffolded as bookshelf-demo (rune new bookshelf-demo --template bookshelf), which ships the 5,000-book dataset this guide leans on.
  • Docker (for one container) and a free port on 5432.

The analysis tools (analysis_ingest, analysis_summarize, analysis_query, analysis_act, analysis_clear, analysis_store) are gated by requiresVectorStorage. Without pgvector, they don’t show up in tools/list — that’s by design (see Analysis Memories).

1. Start pgvector

Drop this docker-compose.yml into bookshelf-demo/ (next to src/server.ts) and run docker compose up -d:

services:
  pgvector:
    image: pgvector/pgvector:pg17
    environment:
      POSTGRES_USER: bookshelf
      POSTGRES_PASSWORD: bookshelf
      POSTGRES_DB: bookshelf
    ports:
      - '5432:5432'

Then apply mcp-rune’s migrations against the new database. The framework ships migrations as data. (ANALYSIS_ENABLED below is only an input to this snippet’s own filter expression — the framework itself never reads it.)

DATABASE_URL=postgres://bookshelf:bookshelf@localhost:5432/bookshelf \
ANALYSIS_ENABLED=true \
npx tsx -e "
import { Pool } from 'pg';
import { migrations } from '@mcp-rune/mcp-rune/db/migrations';
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const needed = migrations.filter(m => m.feature === 'core' || process.env.ANALYSIS_ENABLED === 'true');
for (const m of needed) { await pool.query(m.up); console.log('applied', m.name); }
await pool.end();
"

That creates the three tables the feature relies on: analysis_memories (vectors + JSONB), ingested_records (raw rows + optional per-record embeddings), and ingested_edges (the relationship graph).

2. Wire vector storage into the bookshelf

Open bookshelf-demo/src/server.ts and add the storage init before createServer is called:

src/pool.ts
import { Pool } from 'pg'
import { vectorStorage } from '@mcp-rune/mcp-rune/runtime'
import { createPgvectorAdapter } from '@mcp-rune/mcp-rune/runtime/integrations/pgvector'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })
vectorStorage.initVectorStorage({
  adapter: createPgvectorAdapter({ pool }),
  serviceName: 'bookshelf-mcp',
  version: '1.0.0'
})
import { Pool } from 'pg'
import { vectorStorage } from '@mcp-rune/mcp-rune/runtime'
import { createPgvectorAdapter } from '@mcp-rune/mcp-rune/runtime/integrations/pgvector'
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
vectorStorage.initVectorStorage({
  adapter: createPgvectorAdapter({ pool }),
  serviceName: 'bookshelf-mcp',
  version: '1.0.0'
})

Storage init is only half the gate. The bookshelf template registers just the data and form-strategy tool classes, so the analysis tools also need to be spread into the registry’s toolClasses, with analysisStorageEnabled telling the registry the analysis adapter is live. In the same src/server.ts, extend the existing new ToolRegistry({ … }) call:

src/server-registry.ts
import {
  ToolRegistry,
  DATA_TOOL_CLASSES,
  FORM_STRATEGY_TOOL_CLASSES,
  ANALYSIS_TOOL_CLASSES
} from '@mcp-rune/mcp-rune/tools'
import { vectorStorage } from '@mcp-rune/mcp-rune/runtime'

const toolRegistry = new ToolRegistry({
  toolClasses: {
    ...DATA_TOOL_CLASSES,
    ...FORM_STRATEGY_TOOL_CLASSES,
    ...ANALYSIS_TOOL_CLASSES // add: the analysis tools must be in the class map
  },
  // …keep the template's existing config here (models, serverContext, createApiClient)…
  analysisStorageEnabled: vectorStorage.isVectorStorageEnabled() // add: lets requiresAnalysisStorage tools register
})
import {
  ToolRegistry,
  DATA_TOOL_CLASSES,
  FORM_STRATEGY_TOOL_CLASSES,
  ANALYSIS_TOOL_CLASSES
} from '@mcp-rune/mcp-rune/tools'
import { vectorStorage } from '@mcp-rune/mcp-rune/runtime'

const toolRegistry = new ToolRegistry({
  toolClasses: {
    ...DATA_TOOL_CLASSES,
    ...FORM_STRATEGY_TOOL_CLASSES,
    ...ANALYSIS_TOOL_CLASSES // add: the analysis tools must be in the class map
  },
  // …keep the template's existing config here (models, serverContext, createApiClient)…
  analysisStorageEnabled: vectorStorage.isVectorStorageEnabled() // add: lets requiresAnalysisStorage tools register
})

When the adapter is missing — say you forget DATABASE_URL and skip init — initVectorStorage returns false, isVectorStorageEnabled() stays false, and the six analysis tools simply stay out of tools/list. No crash, no noisy error path.

Behind on migrations? If DATABASE_URL is set but the analysis migrations haven’t been applied (e.g. after upgrading mcp-rune), the analysis tools register but fail at call time with column "embedding" does not exist. Guard against it by calling assertMigrationsCurrent(pool, { features: ['core', 'analysis'] }) before initVectorStorage — it fails fast at startup with the pending list and Run: npm run db:migrate. See Database setup → Detecting drift at startup.

3. Boot the bookshelf with the big dataset

DATABASE_URL=postgres://bookshelf:bookshelf@localhost:5432/bookshelf \
BOOKSHELF_DATASET=large \
npx @modelcontextprotocol/inspector -- npx tsx src/server.ts

Inside the Inspector, confirm the analysis tools are now visible — analysis_ingest, analysis_summarize, analysis_query, analysis_act, analysis_clear, analysis_store should all appear alongside the CRUD tools. If they don’t, double-check initVectorStorage was called with a live adapter and that the registry got both halves from step 2: the ANALYSIS_TOOL_CLASSES spread and analysisStorageEnabled.

4. One strategy at a time

analysis_ingest downloads records once and writes a per-page summary through whichever strategy you pick. After the first ingest, every later call goes through analysis_summarize, which re-runs strategies against the already-stored rows without hitting the API again — the whole point of the two-tool split.

distribution (the default)

analysis_ingest({
  analysis_id: "tour",
  model: "book",
  ingest_all: true,
  per_page: 50
})
// → "Stored 5000 record(s) (8 fields per record) across 100 page(s)."
// → 100 memories written with category "page_summary:distribution".

coverage

analysis_summarize({
  analysis_id: "tour",
  strategy: "coverage",
  max_records: 5000
})
// → category "page_summary:coverage", flags `notes` and `rating`
//   on pages where the missing rate crosses 50%.

anomaly

analysis_summarize({
  analysis_id: "tour",
  strategy: "anomaly",
  max_records: 5000
})
// → category "page_summary:anomaly", surfaces the ~1% of records
//   with extreme `pages` values (|z| > 2) plus rare enum values.

temporal

analysis_summarize({
  analysis_id: "tour",
  strategy: "temporal",
  max_records: 5000
})
// → category "page_summary:temporal", buckets `created_at` by week,
//   reports the recency window, and flags the 60-day gap baked into
//   the generator.

entity-extraction

analysis_summarize({
  analysis_id: "tour",
  strategy: "entity-extraction",
  max_records: 5000
})
// → category "page_summary:entity-extraction", top-N `genre_id`
//   crosswalk for the page.

5. Several strategies in one pass

The same lens menu is available at ingest time. Run all five at once on a fresh analysis_id to see what a multi-strategy ingest looks like end to end:

analysis_ingest({
  analysis_id: "tour-multi",
  model: "book",
  ingest_all: true,
  per_page: 50,
  summary_strategies: [
    "distribution",
    "coverage",
    "anomaly",
    "temporal",
    "entity-extraction"
  ]
})
// → Stored 5000 records across 100 pages.
// → ~500 memories written: one per page per strategy that passed its
//   `appliesTo` check (anomaly needs ≥4 records per page; temporal
//   needs ≥1 ISO-date field; entity-extraction needs ≥1 *_id field).

summary_strategy (singular) and summary_strategies (plural) are mutually exclusive — passing both fails fast at validation time.

5.5 Switch to the graph dataset for the GraphRAG strategies

The remaining four strategies — relationship-coverage, concept-touch, rule-violation, semantic-cluster — read from edges, embeddings, and the domain registry. None of those exist on the flat large dataset. Restart the bookshelf with the graph dataset, which adds author and genre models with proper foreign keys, two DomainConcepts (reading-pipeline, catalogue), and two BusinessRules (completed-books-need-rating, books-need-author). The graph generator deliberately leaves ~5% of books without an author_id and ~15% of completed books without a rating so every GraphRAG strategy has real signal to surface.

Stop the running Inspector, then:

DATABASE_URL=postgres://bookshelf:bookshelf@localhost:5432/bookshelf \
BOOKSHELF_DATASET=graph \
npx @modelcontextprotocol/inspector -- npx tsx src/server.ts

In the Inspector, run a fresh ingest. The new args are hop_depth: 1 (follow declared belongsTo associations) and embed_records: true (the default — embeddings are needed for semantic-cluster):

analysis_ingest({
  analysis_id: "graphrag",
  model: "book",
  ingest_all: true,
  hop_depth: 1,
  embed_records: true,
  summary_strategies: [
    "relationship-coverage",
    "concept-touch",
    "rule-violation",
    "semantic-cluster"
  ]
})
// → Stored 500 record(s) across 10 page(s). Hopped: 10 author, 6 genre.
// → ~40 memories written (one per page per strategy, modulo appliesTo gates).

Four strategy lenses, walked one at a time:

relationship-coverage

analysis_query({
  analysis_id: "graphrag",
  mode: "semantic",
  category: "page_summary:relationship-coverage",
  query: "missing author edge"
})
// → top finding names belongsTo:author coverage % per page (around 94%)
//   and the 1–3 gap IDs per page that lack an author edge.

The relationship-coverage guide explains the per-edge-type stats; the takeaway here is that the gap records surface directly in the finding text so the LLM can analysis_query mode:"filter" where:{id:[...]} to inspect them.

concept-touch

analysis_query({
  analysis_id: "graphrag",
  mode: "semantic",
  category: "page_summary:concept-touch",
  query: "catalogue gap"
})
// → finding contains "catalogue → [author, genre]: 47/50 (94%);
//   per-target author=47, genre=50". The 3-record gap matches the
//   bookshelf's missing-author rate.

reading-pipeline (book + genre) lands at 100% because every book has a genre. catalogue (book + author + genre) lands lower because of the missing-author records. See the concept-touch guide.

rule-violation

analysis_query({
  analysis_id: "graphrag",
  mode: "semantic",
  category: "page_summary:rule-violation",
  query: "completed books missing rating"
})
// → finding names the failing rule + the first few IDs per page:
//   "completed-books-need-rating (warning): N/50 failed (e.g. b127, b223)".
//   "books-need-author (error): 2/50 failed (e.g. b34, b48)".

See the rule-violation guide.

semantic-cluster

analysis_query({
  analysis_id: "graphrag",
  mode: "semantic",
  category: "page_summary:semantic-cluster",
  query: "natural book groupings"
})
// → finding names the cluster representatives by title hint:
//   "cluster 1 (size 14, mean dist 0.18): rep b3 \"Clean Patterns #3\""
//   …

See the semantic-cluster guide.

5.6 Graph-aware sampling with composable stratifiers

With edges + embeddings + concepts in play, analysis_query mode:"sample" can balance a sample across multiple graph dimensions at once. The stratifiers param accepts up to 3 entries that compose with the existing where / proximity / stratify_by partitioning:

analysis_query({
  mode: "describe",
  analysis_id: "graphrag"
})
// → A "Graph dimensions available" section lists registered concepts,
//   observed edge types, and embedding coverage %. (describe takes no
//   model param — it documents the whole session, listing every model.)

A three-stratifier sample, pre-filtered to completed books:

analysis_query({
  mode: "sample",
  analysis_id: "graphrag",
  sample_size: 12,
  where: { status: "completed" },
  stratifiers: [
    { kind: "concept", concept: "catalogue" },
    { kind: "edge",    edge_type: "belongsTo:genre", bucket: "present" },
    { kind: "cluster", k: 3 }
  ],
  model: "book"
})
// → 12 completed-status books, balanced across:
//   - concept (catalogue touched vs not)
//   - edge presence (has genre edge vs not — uniform on bookshelf)
//   - semantic cluster (3 anchor-nearest groups)

When kind: "cluster" is requested on a session that was ingested with embed_records: false, the tool auto-back-fills missing embeddings before sampling. No re-ingest required.

5.7 Path chains and semantic record ranking

The hop-expanded session unlocks the two newest query surfaces. path mode walks the typed edge graph between two records — ask how two books are connected (swap in any two ids you saw in earlier results):

analysis_query({
  analysis_id: "graphrag",
  mode: "path",
  from_model: "book",
  from_id: "b34",
  to_model: "book",
  to_id: "b17",
  max_depth: 4
})
// → "1 path(s) between book/b34 and book/b17 (shortest: 2 edge(s)):
//    - book/b34 —belongsTo:author→ author/a7 ←belongsTo:author— book/b17"

Traversal is undirected over the directed edges, and each step renders with the direction it used — the chain above reads “b34 and b17 share author a7”. When nothing connects within max_depth (default 4, capped at 6), the tool says so and suggests re-ingesting with a deeper hop_depth.

semantic with target: "records" ranks the ingested rows themselves by meaning — and composes with where for hybrid semantic + filter retrieval in one SQL statement. Because graphrag holds three models, the model param is required (the error would name the candidates):

analysis_query({
  analysis_id: "graphrag",
  mode: "semantic",
  target: "records",
  model: "book",
  query: "space exploration epics",
  where: { status: "completed" },
  min_similarity: 0.3,
  top_k: 5
})
// → "Top 5 record(s) by similarity to \"space exploration epics\":
//    - [61.2%] {\"id\":\"b201\",\"title\":\"Voyages Beyond #12\",\"status\":\"completed\",…}"
//   …plus an embedding-coverage footer when not every record is embedded.

Only the query string pays for an embed. Because the §5.5 ingest ran with embed_records: true (the default), every record was embedded at write time; on a session ingested with embed_records: false, each records-targeted query back-fills up to 500 missing embeddings before ranking. min_similarity is an optional floor — omit it for rank-only results.

6. Recall by category

Every memory is written with category: "page_summary:<strategy>", which lets analysis_query mode: semantic filter cleanly:

analysis_query({
  analysis_id: "tour-multi",
  mode: "semantic",
  category: "page_summary:anomaly",
  query: "outlier page count"
})
// → only the anomaly memories rank, no distribution noise in the results.

Drop the category to cross-cut all strategies:

analysis_query({
  analysis_id: "tour-multi",
  mode: "semantic",
  query: "missing metadata"
})
// → ranks coverage memories highest, with relevant distribution and
//   anomaly memories trailing.

7. Teardown

analysis_clear cascade-deletes both tables for the session:

analysis_clear({ analysis_id: "tour" })
analysis_clear({ analysis_id: "tour-multi" })
// → "Cleared 5000 ingested record(s) and N finding(s) …"

docker compose down -v drops the container and the volume — by design the analysis loop is opt-in infrastructure, not a permanent dependency.

Where to go next

  • Summary Strategies — full catalog with links to each strategy’s deep-dive guide, the SummaryStrategy contract, and a walk-through for shipping your own strategy via an ApiExtension.
  • Per-strategy guides linked from the Summary Strategies catalog — one file each for the nine built-ins; explains the algorithm, inputs consumed, output shape, and edge cases.
  • Analysis Memories — the full feature reference: ingest modes, the six-mode analysis_query API, stratified sampling, analysis_act, lifecycle and retention.
  • Proximity Sampling — date-windowed bucketed sampling for the analysis_query mode: sample path.
  • Domain Knowledge Guide — how DomainConcept and BusinessRule feed concept-touch and rule-violation.