Derived from content-mcp-technical-design.md.
Each top-level checkbox is one implementation step. Steps are processed
top-to-bottom by the roadmap-execute skill and build on each other, so the
order matters. Section references (§) point into the design document.
-
Project skeleton — standalone Spring Boot app on top of
spring-services- Own repo/app:
java-parentas Maven parent,spring-services(1.3.x) as dependency (§1a). ContentMcpApplicationwith@SpringBootApplication,@EnableScheduling, and@Import({ McpConfiguration.class, SearchConfig.class })(§1a).- Add
org.jsoup:jsoupdependency (§9). No fork ofspring-services— dependency + import only (§2). application.yamlscaffolding; emptycom.openelements.contentpackage with aContentConfig@Configuration(§3).- Verify the app boots and the existing
/mcpendpoint is exposed by the imported config.
- Own repo/app:
-
ContentSourceabstraction + configuration propertiesContentSourcerecord:id,type(website|git),baseUrl,sitemaps,urlInclude/urlExclude,contentSelector,contentExclude, locale derivation,enabled(§3, §3a).ContentSourcePropertiesas@ConfigurationProperties("open-elements.content")incl.refreshCron,userAgent,rateLimitPerHost,requestTimeout,maxBodyBytes,enabled(§6, §10).- Ant-glob matching for
urlInclude/urlExcludeagainst the URL path via SpringAntPathMatcher; default include["/**"](§6 pattern language). - Bind the example OE source from §6 in
application.yaml.
-
ContentDocumentdata model + Meilisearch index settingsContentDocumentrecord matching the canonical JSON:id,source,locale,url,title,excerpt,body,author,categories,publishedDate,lastmod,previewImage(§4).- Stable primary key = hash of
source + url(§4). ContentIndexSettings@Bean IndexSettings: searchable["title","excerpt","body"], filterable["source","locale","author","categories","publishedDate"], sortable["publishedDate"],publishedDate:desctie-breaker (§4, §8).- Index name via
MeilisearchProperties.resolveIndex("content")(§4).
-
SitemapCrawler— URL discoveryRestClient-based reading of the sitemap index + child sitemaps → all<loc>+<lastmod>(§5.1).- Filter candidates against the source's
urlInclude/urlExcludepatterns (§5.1, §6). - Fallback when no sitemap: use
baseUrlas entry point, follow internal links withinurlIncludescope with bounded depth (§6).
-
PageFetcher— robust HTTP fetchingRestClientGET with UAOpenElementsContentBot/1.0 (+https://open-elements.com),If-Modified-Since/ETag, timeout,maxBodyBytes(§5.3, §10).- Rate limit per host (≤2 req/s), retry with backoff (§5.3).
- Handle 304 Not Modified as "unchanged" (§5.3).
-
ContentExtractor— jsoup content + metadata extraction- Select main container via
contentSelector(comma list, first match wins); always strip<script>/<style>/<noscript>/<template>/comments; applycontentExclude(§6content-selector). - Whitespace-normalize to clean text/markdown for
body; readability-style largest-text-block fallback when no selector matches (§5.4, §6). - Metadata independent of
contentSelector:<meta>(OpenGraph/Article), JSON-LD,<time datetime>→ title/date/author/categories/excerpt (§6 metadata).
- Select main container via
-
ContentSourceStrategyinterface +WebsiteSourceStrategyContentSourceStrategywithdiscover(src) → List<DiscoveredItem>andfetch(item) → ContentDocument(§3a).WebsiteSourceStrategywiringSitemapCrawler+PageFetcher+ContentExtractor; one strategy pertype, selected fortype: website(§3a).
-
ContentIndexer— orchestration, diff & upsert/delete- Drive Discover → Diff → Fetch → Extract → Index through the strategy interface (§3a, §5).
- Diff
lastmodagainst the value stored in the index: fetch only new/changed URLs; delete documents whose URLs disappeared from the sitemap (§5.2). - Map to
ContentDocumentand write viaBatchWriter(addDocumentsbatched,waitForTask); idempotent upsert by stableid(§5.5, §11).
-
ContentBootstrapStep— initial reindex- Implement
SearchIndexBootstrapStep; runs insideMeilisearchBootstrapRunner, setsSearchReadinessStateonly after completion (§5.6, §11). - A single failing page skips only that document; bootstrap still completes (§11 fault tolerance).
- Implement
-
ContentRefreshScheduler— incremental re-crawl@Scheduled(cronrefresh-cron, default hourly) incremental re-crawl via thelastmoddiff, reusingContentIndexer(§5.6, §6).
-
ContentSearchService— search facade- Facade over
MeilisearchClient.multiSearchwith filters (source,locale,categories, date) andHighlighter.safeHighlighton_formattedsnippets (§8). - Ranking: searchable order
title > excerpt > body,publishedDate:desctie-breaker (§8).
- Facade over
-
ContentMcpToolProvider— the 4 MCP tools- Implement
McpToolProvider.toolSpecifications()so tools appear automatically on/mcp(§2, §7). search_content(queryreq,locale?/source?/category?, paging) → title, URL, date, highlighted snippet, score (§7).list_posts(locale?/source?/category?/since?/paging) sortedpublishedDate:desc(§7).get_post(urlorid) → full body + metadata;list_categories(source?/locale?) via facets (§7).- Argument parsing/paging via
McpTools/McpPaging/McpToolSupport(§7).
- Implement
-
Read-only scoped Meilisearch key for the content index
- Dedicated read-only scoped key limited to the content index via
MeilisearchScopedKeyInitializer/createScopedKey; writes only through the indexer (§8 security).
- Dedicated read-only scoped key limited to the content index via
-
Operations & robustness hardening
- Respect
robots.txtper host (§11). - Observability: logging/metrics analogous to
BatchWriter(documents pushed, task status) (§11).
- Respect
- Add
hiero.org/blogand Support & Care as config-only sources- New
application.yamlsource entries forhiero(/blog/**, selectormain article) andsupport-and-care(content-selector: body+content-exclude) — no new code (§6, §12 P2). - Selector fine-tuning per site; verify extraction quality; resolve the §"Open questions" (hiero sitemap presence, Support & Care page count) before finalizing.
- New
-
type: gitsource withGitSourceStrategy- Extend the config schema with
type: git(provider,repo,ref,paths,base-url,token) (§13). GitSourceStrategy: GitHub Trees API discovery (git/trees/{ref}?recursive=1) filtered bypaths, contents via raw endpoint usingRestClient(§13 discovery & fetch).- Incremental via per-file commit SHA as the
lastmodequivalent (§13 incremental). - Extraction: YAML frontmatter → metadata, Markdown body direct, resolve/clean Hugo shortcodes; URL mapping file path → canonical URL; locale from filename suffix (§13 extraction & URL mapping).
- Secrets: token only from env/secret store (
${GITHUB_TOKEN_*}), never logged, server-side only, read-only fine-grained scope (§13 secrets). - Everything downstream (
ContentDocument→ index → 4 tools) stays unchanged (§13).
- Extend the config schema with
- Facets, synonyms, and semantic search add-on
- Per-language synonyms/stop words; category facets surfacing (§8, §12 P4).
- Optional embeddings-based semantic search as an add-on (§12 P4).