Sign In

poliety-mcp

Package Overview
Dependencies
Maintainers
1
Versions
4
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

poliety-mcp

MCP server for The Poliety, a daily AI news briefing. Gives AI agents structured access to editions, change tracking, integrity verification, and governed ecosystem events with inspectable evidence via stdio.

latest
Source
npmnpm
Version
1.3.0
Version published
Weekly downloads
629
Maintainers
1
Weekly downloads
 
Created
Source

The Poliety

Poliety is a news feed for AI agents. Humans can read it too.

The Poliety is a daily news briefing on AI, startups, developer tools and open source. It aggregates from named, linked sources (RSS feeds, the Hacker News API, the GitHub API and arXiv) and layers original editorial briefs on top, written each morning by Claude Code (Sonnet 5) running headless on GitHub Actions. The site at poliety.com is fully static and free forever; a separate paid keyed API at api.poliety.com adds memory: change cursors, wire history and filtered queries.

Architecture

config/sources.json     what we pull, with outbound fetch limits
scripts/fetch.mjs       ingestion: fetch, sanitize, dedupe, classify, score
data/items.json         the day's wire (sanitized, plain text only)
data/wire/*.json        committed daily wire snapshots (the compounding archive)
data/editorial/*.json   daily editions written by the editor (Claude Code)
scripts/lib/hash.mjs    sha256 content hashes (the integrity layer)
scripts/build.mjs       static site generator (site, feeds, edition archive)
public/                 the deployable site (HTML, CSS, feeds, JSON API)
scripts/serve.mjs       production origin server on Railway (frozen trust anchor)
scripts/api/            the separate keyed API service (api.poliety.com)
scripts/mcp/server.mjs  MCP server: stdio adapter over the API for AI assistants

Everyday commands:

npm run fetch    # pull the wire (also writes today's data/wire snapshot)
npm run build    # render the site into public/
npm run daily    # both
npm run serve    # run the production server locally (http://localhost:8642)
npm run api      # run the API service (DB_PATH, INGEST_URL, PORT env)
npm run mcp      # run the MCP server (POLIETY_API_KEY env)
npm test         # full test suite (sanitizer, classifier, hashes, API, MCP)

data/wire/ and data/editorial/ grow by one small JSON per day and are committed on purpose: they are the archive the API's history products are built from and are not prunable without breaking published edition URLs.

The daily edition

The edition publishes itself from GitHub Actions. .github/workflows/edition.yml (cron 10:30 UTC) runs the /edition slash command headless with claude -p on Sonnet 5, authenticated by the ANTHROPIC_API_KEY secret (see .claude/commands/edition.md): fetch the wire, pick and verify stories, write data/editorial/YYYY-MM-DD.json in AP style, rebuild, record the verdict. Every editorial write passes through the vendored Bench governance hook, and the job refuses to publish unless the editorial changes carry fresh .bench/entries receipts and both hash chains verify. It then commits the day's content directly to protected main using the EDITION_PUSH_TOKEN secret (the one sanctioned direct-to-main write) and confirms the result went live with scripts/verify-live.mjs.

Push to main deploys the services the commit touches. The static site (poliety) and the API (poliety-api) are repo-connected on Railway. Since 2026-08-12 each service deploys only when a push matches its build.watchPatterns (railway.json for the site, railway.api.json for the API), so a test-, docs- or ledger-only commit deploys nothing and the daily edition commit deploys just the site. The static service's build command is npm run daily, so each of its deploys re-fetches the wire. There is no staging and no separate publish step: the edition's daily commit is also its deploy. Green CI on a branch says nothing about what is live; compare beacon.builtAt in /api/latest.json against your latest commit.

The manual fallback is tools/daily.cmd on the owner's PC: the same /edition playbook (with a wire-only refresh if Claude Code is unavailable), then the verdict, then a backup commit, then railway up --service poliety. Its Windows Task Scheduler job (registered once with powershell -ExecutionPolicy Bypass -File tools\register-schedule.ps1) is disabled now that the Actions path is verified. The fallback runs in this order on purpose: build, record the verdict, back up, then publish. The backup commit is the only copy of an edition outside the running container, so it happens before the step that can fail. Each phase writes a step: marker to logs/daily-YYYY-MM-DD.log, and the last line of a healthy run is daily edition finished. If that line is missing, the script stopped after the last marker.

Verify the deployed site, not the build output. npm run verify (scripts/verify-live.mjs) checks the live origin: beacon presence and a sane age, CORS on the edition JSON, HSTS, the social card served as image/png, HTML still same-origin, the archive index, the integrity head and its freshness lag, and every URL llms.txt publishes. npm run verify -- --after <deploy-start-iso> --wait 20 polls until the live build postdates a specific deploy. It is Node rather than PowerShell on purpose: ConvertFrom-Json silently converts ISO date strings to local DateTime objects, so timestamp comparisons come out wrong by the machine's UTC offset and still look plausible.

call is load-bearing in daily.cmd. railway and npm resolve to .cmd batch files, and cmd.exe transfers control to a batch file invoked without call and never returns. A missing call on the railway line silently ended the script at the publish step on every run: the site deployed, the backup never executed, no Daily edition backup commit was ever created in the repo's history, and Task Scheduler still reported success because railway.cmd exited 0. claude and git are .exe and do not need it. Anything added to that script must be checked with where <cmd> first.

Security model

The attack surface is deliberately close to zero:

  • Fully static output. No server code, no database, no forms, no accounts, no cookies, no client-side JavaScript at all. There is nothing to inject into and nothing to steal.
  • Hostile-input pipeline. Every string from every feed is sanitized on arrival (tags stripped to a fixed point, entities decoded safely, control characters removed) and HTML-escaped again at render time. URLs must parse as http(s) with no embedded credentials or they are dropped.
  • Strict CSP. default-src 'none' with narrow style/img allowances, plus nosniff, frame denial, no-referrer and a locked Permissions-Policy. All of it comes from scripts/lib/headers.mjs, applied by scripts/serve.mjs at the origin, with a meta CSP fallback in the HTML.
  • One source for headers, because two disagreed. public/_headers is the Netlify and Cloudflare Pages convention and is inert on Railway: production serves from serve.mjs, which reads no such file. A CORS rule added to _headers alone therefore did nothing while looking correct in review. Both the generated file and the runtime server now derive from PATH_RULES in scripts/lib/headers.mjs, and tests/headers.test.mjs asserts they agree, that no per-path rule can drop a baseline header, and that each rule's glob matches its own runtime matcher.
  • CORS on the machine-readable outputs only. /api/*, both feeds, llms.txt, sitemap.xml and robots.txt send Access-Control-Allow-Origin: * and Cross-Origin-Resource-Policy: cross-origin; serve.mjs also answers OPTIONS preflight on those paths. Without it a browser-based agent gets a CORS failure on the free edition, which would make "agent-first" false in the one place it is easiest to check. There is nothing to protect: this origin has no cookies, no credentials and no per-user data, and the files are free by policy, so a cross-origin reader obtains exactly what a plain GET already gives it. HTML pages keep the strict same-origin default.
  • HSTS, served for real. max-age=31536000; includeSubDomains on every response. It sat in the inert _headers file for a long time, declared and never delivered, which meant the repo claimed a protection the site did not have. includeSubDomains binds every poliety.com subdomain to HTTPS in browsers for a year; both that exist are HTTPS behind Cloudflare, and it is walked back by lowering max-age if that ever changes. preload is deliberately excluded and pinned out by a test: leaving the browser preload list takes months, which makes it the one genuinely irreversible piece, and it buys a site with no accounts and no cookies nothing.
  • No third-party requests. No CDN fonts, no analytics, no external scripts: nothing to supply-chain.
  • DDoS posture. Static files behind any CDN (Cloudflare, Netlify, GitHub Pages) absorb traffic floods; there is no origin logic to overwhelm.
  • One pinned runtime dependency (rss-parser), audited in CI with npm audit --audit-level=high.
  • Prompt-injection hygiene. The editorial agent treats fetched content as data, never as instructions (see .claude/commands/edition.md).

The one binary asset is static/root/og-card.png, the 1200x630 social preview card. It is same-origin, referenced only from meta tags and never rendered by any page, so "no scripts, no cookies, no trackers, no third-party requests" all still hold literally. _headers overrides site-wide Cross-Origin-Resource-Policy: same-origin to cross-origin for that one path, because a link preview is by definition fetched by someone else's origin.

See SECURITY.md for reporting.

Editorial verdicts

data/verdicts/YYYY-MM-DD.json records what the house style found in each edition. The shape is poliety.verdict.v1 (scripts/lib/verdict.mjs), written by npm run verdict after a build. It is not published to the site and does not gate publication: it records what shipped.

Three properties make a record worth keeping, all borrowed from patterns already in this repo rather than invented:

  • It names the policy it applied. policyHash covers config/site.json#editorial and .claude/commands/edition.md, the way a Bench ledger entry names its constitution. Rules change; a verdict that does not say which rules it used is an opinion with a date on it.
  • It binds to content by hash. Each subject carries the brief's contentHash, the same digest the public edition publishes, so a brief cannot be edited after the fact and keep wearing its verdict. Subjects come from the built edition documents, so there is one sanitizer (in build.mjs) and this reads its output.
  • A rule that was not checked is skipped, never pass. Most of the house style needs a reader who checked the sources, so an honest record is mostly skips. CHECKS declares every rule including the ones no machine can decide, so the gap between what the style guide demands and what was verified is visible in the record instead of implied away.

Mechanical today: banned dashes and headline terminal periods (block), named linked sources and http(s) source URLs (block), two-source preference, lede length and AI-pattern language (warn). Judgment, recorded as skipped: whether facts are supported by their sources, attribution accuracy, reporting versus opinion, story selection.

Records chain by prevHash/recordHash, and the recorder validates every existing record on load and refuses to write on top of a broken one. Being straight about what that chain is worth: it catches corruption and makes an after-the-fact edit obvious in a diff, but anyone with the repo can rewrite the series with --recheck. Git history is the real guarantee, which is why these are committed.

The fallback tools/daily.cmd treats a non-zero exit from the recorder as a logged warning rather than a failed edition. The recorder describes what shipped; it must not be able to cost a day's publication. It writes through a temp file and renames, because the daily job stages data/verdicts into a commit unattended and a process killed mid-write must not leave a truncated record that gets committed as real.

/standards.html is the public half: the rule list, which rules are machine-verified on every edition, which need a person, and what a passing record does not mean. It renders from the same CHECKS registry the recorder runs, so the page cannot claim a rule the code does not check or omit one it does.

Records themselves are not published yet, and the page says so and why. Publishing a measurement turns it into a target: the cheapest way to clear a two-source rule is to staple a second link onto a single-source story, which moves the number and improves nothing. They go public once the gate includes the judgment checks, so that what a reader gets is an audit rather than a spell-check with a certificate.

Integrity chain

The API stores every ingested edition with a sha256 over its exact bytes and a prevHash linking it to the nearest earlier date. GET /integrity.json publishes the head, the root and the chain length unauthenticated, with a one-line witness recipe; the full chain stays behind the keyed /v1/editions. The endpoint carries an ETag derived from the head hash and chain length, deliberately not from generatedAt, so a watcher polling with If-None-Match gets a 304 until the chain actually moves.

Verification, no key required:

curl -sS https://poliety.com/api/latest.json | shasum -a 256
curl -sS https://api.poliety.com/integrity.json

The digest equals chain.head.contentHash when the site and the chain are in sync. Record chain.head daily and the next day's prevHash must equal what you recorded.

The guarantee is stated narrowly in the payload's scope block, on purpose. Editions for dates before the head never change. The head itself is the current date's row and is replaced on every site rebuild, so its hash moves during the day; a witness records it after the date closes or verifies it through the next day's prevHash. And the chain is served by the operator that writes it, so it proves internal consistency and makes a silent rewrite detectable to anyone who recorded an earlier head. It is not a notary. Signed editions and a transparency log are V3.

publicSourceUrl() in scripts/api/server.mjs launders INGEST_URL before it appears in the payload: userinfo, query strings and non-http(s) schemes cause the source and the recipe to be withheld rather than published, because that field is operator config being echoed into an unauthenticated document.

Social previews

Every page carries Open Graph and twitter:card metadata (page() in scripts/lib/render.mjs). X reads twitter:* first and falls back to og:*, but two things have no fallback: twitter:card has no Open Graph equivalent, so the card type must be declared, and the image chain ends at og:image with nothing behind it. Title and description degrade all the way down to <title> and <meta name="description">; an image does not. Without both tags a shared link unfurls as bare text, which is what happened before this existed.

Article pages emit og:type: article with article:published_time from the brief. Everything else is website. The card image is deliberately static rather than per-article: generating one image per brief would mean an image library, and this project runs one production dependency. X strips or shrinks headline text on link previews anyway, which is why the wordmark and tagline are baked into the card art.

The card was rendered once via GDI+ on Windows and committed; nothing in the build depends on that. To change it, redraw at 1200x630 (1.91:1, PNG, under 5MB) and replace the file. X retired its Card Validator, so verify by posting the link in a DM to yourself or using a third-party Open Graph debugger.

For AI agents

The site is machine-readable by design: /llms.txt, /feed.json (JSON Feed 1.1), /feed.xml (RSS 2.0), /api/latest.json (stable poliety.edition.v1 schema with per-item sha256 contentHash), a per-day edition archive at /api/editions/YYYY-MM-DD.json indexed by /api/editions/index.json (a static host has no directory listing, so without the index the archive URL published in llms.txt was a 404 an agent would walk into on its first hop; serve.mjs also falls back from index.html to index.json for directory requests), and schema.org NewsArticle markup on articles. Crawlers, including AI crawlers, are welcome per /robots.txt.

/llms.txt carries the free-vs-paid rule as an if/elif branch an agent can execute, not as prose (decisionBranch() in scripts/build.mjs, comment alignment computed so it survives config changes). The boundary it states: free is the complete present, and the key buys memory. An agent that polls the free edition and diffs contentHash values can build its own change feed, which the hashes exist to enable. What it cannot do is reconstruct days it was not watching, or backfill firstSeenAt. That is the whole line, and saying it plainly is better than implying the free tier is crippled when it is not.

Every edition JSON carries a beacon object (scripts/lib/beacon.mjs): builtAt, staleAfter, brief and wire counts, source counts, trailing averages over the last 7 committed days, named checks and an ok/degraded status. It is the output-side twin of sourceHealth, which reports whether each wire source answered; the beacon reports whether what we published from them looks like a normal edition. The documented rule is now > staleAfter || status !== "ok": consumers must combine age with the flag, because whatever kills the build also stops the beacon updating, so a dead pipeline leaves a stale ok behind. Volume checks stand down until three prior days are on file rather than firing on thin history, and the beacon is a pure function of each edition's own inputs plus the days before it, so archived editions rebuild to identical bytes.

Event layer

scripts/events/ watches a curated registry of primary sources (config/events.json: vendor status pages, SDK release feeds, pricing docs) and publishes governed CanonicalEvent records to data/events/log/, hash-chained on their own spine (data/events/chain.json, verified by scripts/events/verify.mjs), deliberately separate from the edition's integrity chain. Detection is deterministic first (meaning-surface hashing in lib/surface.mjs; statuspage transitions never consult a model) with one bounded claude-sonnet-5 call for meaning-vs-content judgment on the rest. A deterministic evidence evaluator (lib/evaluate.mjs) applies the sourcing floor and consults the vendored wisdom system for domain confidence and accumulated cautions, recording every outcome back as an experience; high materiality with weak confidence publishes as evaluating, never verified. Every event is adjudicated by the vendored Bench tribunal (govern.py, constitution layer config/bench-events.json) before its file is written; the receipt id is in the event and the receipt itself is in .bench/entries. Events are immutable once written: corrections supersede via a new chained event, and index.json is the derived current-status view. Runs daily from edition.yml and rides the edition's publish commit. The static site republishes the chained event files byte-identical under /api/events/, the API self-ingests them link-by-link verified to serve unauthenticated GET /v1/events plus the keyed evidence and history endpoints, and the MCP event tools sit on top (see the milestone log in ROADMAP.md).

The paid API

scripts/api/ is a separate service (Railway, api.poliety.com) that polls the site's own public latest.json, stores wire history in SQLite (built-in node:sqlite, zero new dependencies) and serves keyed /v1 endpoints: latest with ETag/304, changes with opaque cursors and section/score filters, wire history queries, an editions hash chain, per-item event history and a usage transparency meter. It also hosts the Agent Concierge at POST /v1/concierge (scripts/api/lib/concierge.mjs): grounded Q&A about the service for incoming agents, open without a key under strict per-client limits and a global daily cap, answered by one Haiku 4.5 call over a grounding corpus built from the same config the human pages render. Answers declare themselves non-canonical (canonical: false, contractual: false) and transcripts (question, answer, timestamp, derived key id when keyed; never IPs) are stored for the operator readout at /admin/concierge. Checkout is hosted by Stripe ($19 a month, founding price); the operator mints plt_live_ keys bound to Stripe subscriptions, only sha256 hashes are stored, and lapsed subscriptions revoke within a day via a background status sweep. See SECURITY.md for the service's threat table and api.poliety.com/docs for the endpoint reference. The free site never depends on the API: if the API dies, poliety.com does not notice.

Required env on the API service (Railway): HOST=0.0.0.0 (the server binds loopback otherwise and the healthcheck fails), DB_PATH=/data/api.db, BACKUP_DIR=/data/backups, STRIPE_KEY (subscription status checks), ADMIN_TOKEN (32+ chars, gates key minting). Optional: ANTHROPIC_API_KEY (enables the concierge; absent means /v1/concierge 404s and nothing dials out) and CONCIERGE_DAILY_CAP (global questions per day, default 500; past it the endpoint answers 503 rather than eating model cost). railway.api.json cannot set env vars, so these live in the service settings.

MCP server

scripts/mcp/server.mjs is a stdio adapter that wraps the paid API for MCP-compatible AI assistants (Claude Desktop, Cursor, any MCP client). Zero npm dependencies, Node built-ins only, JSON-RPC 2.0, MCP protocol 2024-11-05.

Seven tools:

ToolWhat it doesAuth
poliety_latestCurrent edition: lead story, briefs, scored wire itemsNo key (a key adds firstSeenAt)
poliety_changesWhat changed since your last check (cursor-based deltas, section/score/kind filters)Key required
poliety_integrityIntegrity chain proof: head hash, root, length, witness recipeNo key
poliety_eventsCanonical events: governed, hash-chained records of material ecosystem changes, cursor-based with type/entity/materiality filtersNo key
poliety_event_evidenceOne event's full evidence package: epistemic detail, tribunal receipt, integrity hashes, provenanceKey required
poliety_event_historyOne event's supersession lineage: what was believed when, and what corrected itKey required
poliety_askThe Agent Concierge: grounded Q&A about the service itself (coverage, capabilities, tiers). Answers are non-canonical; canonical_urls in the response lists the citable sourcesNo key (a key raises the limit)

Add to your MCP client config:

{
  "mcpServers": {
    "poliety": {
      "command": "node",
      "args": ["/path/to/poliety/scripts/mcp/server.mjs"],
      "env": { "POLIETY_API_KEY": "plt_live_..." }
    }
  }
}

Set POLIETY_API_KEY to your API key (get one at https://poliety.com/api.html). The latest, integrity, events and ask tools work without a key: keyless poliety_latest reads the free public edition, and a key upgrades it to the paid API's copy with firstSeenAt timestamps. If the key is missing, the remaining tools return an error carrying the machine-readable offer (https://api.poliety.com/offer.json), not a crash.

The server logs to stderr and never logs the API key. POLIETY_API_URL overrides the default https://api.poliety.com for testing.

Editorial standards

AP Style. Sentence-case headlines. Every fact attributed to a named, linked source. Original briefs synthesize at least two sources when possible. No em dashes. Corrections are noted on the page. See /about.html on the built site.

Keywords

mcp

FAQs

Package last updated on 12 Aug 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts