feat(mcp-catalog): add Unstructured Transform MCP server#1
Conversation
A manual done (dashboard/desktop drag) runs complete_task but ends no run, so a trailing crashed/crashed run history never gains the 'completed' outcome that breaks the repeated_crashes streak — the card kept flagging "needs attention" forever after being finished. repeated_failures had the same hole via a stale counter. Terminal statuses are now exempt from both: done means done; the history stays on the event log for audit. Regression test included.
An inherited HERMES_TUI=1 or a `display.interface: tui` config default sent kanban workers into the Ink TUI, whose no-TTY bail-out exits 0 without doing the task — every attempt ended in "protocol violation". Two layers: - _default_spawn pins `--cli` (highest-precedence interface flag) and strips HERMES_TUI from the child env (covers older builds on PATH). - _resolve_use_tui gates ambient TUI prefs (env/config) behind a real TTY; an explicit --tui still wins so the informative bail-out stays reachable.
A retried task (→ running) kept showing "crashed Nx": the in-flight run has no outcome yet, so the trailing crash scan skipped it and kept counting the prior streak, and the consecutive_failures counter lingers. Exempt `running` from both repeated_failures and repeated_crashes so a fresh attempt clears the banner until it itself resolves (re-fires if the new run also fails).
Dragging a task done→ready did nothing: the respawn guard saw a run that completed within the success window and deferred forever, unable to tell a deliberate operator re-run from a status flap. Now a re-queue event (status change, promote, unblock, reclaim) AFTER the completion bypasses the recent_success guard, so an explicit done→ready runs again.
…h fix The earlier fix gated _resolve_use_tui, but the EARLY launcher (_wants_tui_early) decides TUI from display.interface before cmd_chat runs — so a `display.interface: tui` default still booted the Ink UI for headless spawns (kanban workers), whose no-TTY bail-out exits 0 → "protocol violation". Gate the early resolver on a real TTY: headless stdio never boots the TUI regardless of config; explicit --tui still does.
…ker-headless fix(kanban): headless workers, live-retry diagnostics, and re-queue respawn
…parse (NousResearch#60591) Port from openai/codex#31188: a parse failure in a policy-bearing config file must not silently replace the effective policy with an empty/default one. Codex's load_exec_policy_with_warning replaced the whole exec policy with Policy::empty() when a .rules file failed to parse, silently dropping managed prompt/forbidden rules; the fix preserves the managed policy while still warning. Hermes had the same bug shape in load_config(): a YAML parse error made _load_config_impl() fall through to DEFAULT_CONFIG, dropping every user override — including approvals.deny rules, which are documented to block commands even under --yolo. In a long-running gateway, a user mid-editing config.yaml into broken YAML silently disarmed their own deny rules on the next load. Now, when the process has a last successfully loaded config for that path (_LAST_EXPANDED_CONFIG_BY_PATH), a parse failure keeps serving it (cached under the corrupt file's signature so the broken file isn't re-parsed) and the warning says edits are being ignored until the YAML is fixed. Fresh processes with no last-known-good keep the existing DEFAULT_CONFIG fallback and warning. E2E-verified: deny rule 'curl*evil.com*' still blocks after mid-process corruption; fixed file reloads normally; fresh-process fallback unchanged.
…se_url switch_model() unconditionally set agent.provider but only set agent.base_url when the resolved value was truthy. When a real provider change resolved an empty base_url (e.g. minimax after copilot), the agent ended up with provider="minimax" but base_url still pointing at api.githubcopilot.com. That incoherent pair then got snapshotted into agent._primary_runtime, so it kept re-applying on every subsequent turn via restore_primary_runtime() until the process restarted. try_activate_fallback() and _swap_credential() were audited and confirmed unaffected: both always derive base_url from an actually constructed client, never from a possibly-empty resolver hint. Fix: when base_url is empty AND the provider is genuinely changing, raise ValueError instead of silently keeping the old provider's URL. This routes through switch_model()'s existing snapshot/rollback path, and callers (tui_gateway/server.py's _apply_model_switch) already catch and surface a clean "switch failed, staying on X" message. Re-selecting the SAME provider with an empty base_url (credential-only refresh) still keeps the current URL, unchanged. Fixes NousResearch#47828 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The HTML session export interpolated the tool-call name into the page without escaping, while every sibling field went through _escape_html. A tool-call name is attacker-influenced, so a prompt-injected model can emit a name containing HTML that executes when the export is opened in a browser. Escape the tool-call name like the other fields.
…okepoint + AUTHOR_MAP The salvaged fix sets HERMES_YOLO_MODE in main()'s dispatch path before _prepare_agent_startup(); this follow-up also sets it inside _prepare_agent_startup() itself so every launcher that triggers plugin/tool discovery (incl. the Termux fast-CLI path) gets the same ordering guarantee before tools.approval freezes _YOLO_MODE_FROZEN (NousResearch#60328).
A cron record authored by a direct jobs.json edit that bypassed
add_job() can lack an "id" key (older writers used "job_id"). Every
site in _get_due_jobs_locked indexes job["id"] eagerly — both the
logging helpers (job.get("name", job["id"]) evaluates the default
argument unconditionally) and the 'for rj in raw_jobs: if rj["id"] ==
job["id"]' persistence loops. A single malformed record therefore
raised KeyError mid-tick, aborting the entire scan before save_jobs()
ran. Result: healthy jobs' fast-forwarded next_run_at was computed in
memory then discarded on the exception unwind, freezing the whole
profile's scheduler in a per-minute loop (observed dormant for weeks).
Fix: normalize id-less records at the top of _get_due_jobs_locked before
anything keys off job["id"] — recover the id from a drifted "job_id"
key when present, else synthesize one via uuid4, and persist. This
repairs the whole bug class at the source rather than guarding each of
the ~12 downstream index sites.
Adds a regression test that fails with KeyError on the current code and
passes with the fix, asserting a healthy sibling job is still returned
when an id-less record shares the store.
A job record in jobs.json can have a non-dict 'schedule' value (null, string,
etc.) from direct edit or old writers.
In _get_due_jobs_locked:
schedule = job.get('schedule', {})
kind = schedule.get('kind')
This (and direct schedule['kind'] in compute_next_run etc.) raises and
aborts the entire due-jobs scan before save_jobs() or advancing next_run_at
for healthy jobs. Exactly the same failure mode as the id-less job P1.
Fix: normalize non-dict schedules to {} early (before any use), matching the
defense added for id-less records. Also added defensive guards in compute
functions.
Added regression test that a bad schedule does not crash and healthy sibling
is still returned.
Refs similar pattern in NousResearch#61382.
One bad next_run_at value in jobs.json aborts the due-jobs scan with ValueError from fromisoformat, before any save_jobs, so siblings lose progress (fast-forwards etc). Early normalization in _get_due_jobs_locked + defensive parses in compute_next_run / _recoverable_oneshot_run_at. Added test_bad_next_run_at_does_not_crash_or_block_sibling_jobs.
… class Structural completion of the malformed-job freeze fixes (NousResearch#61382 id-less, NousResearch#61525 non-dict schedule, NousResearch#61581 bad next_run_at): wrap the per-job body of _get_due_jobs_locked in try/except so any FUTURE malformed-field variant degrades to skipping that one job for the tick instead of aborting the scan before save_jobs() and freezing the whole profile's scheduler. Also: restore test_repeated_concurrent_runs_accumulate_completed_count to TestMarkJobRunConcurrency (accidentally re-parented by the NousResearch#61581 diff), add a containment regression test, and AUTHOR_MAP for hydracoco7. E2E: one jobs.json carrying all five malformed shapes (drifted job_id, missing id, null schedule, garbage next_run_at, non-string last_run_at) plus a healthy sibling — single tick contains all five, sibling fires, repairs persist, second tick stable. 670 cron tests green.
There was a problem hiding this comment.
1 issue found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="optional-mcps/unstructured-foundation/manifest.yaml">
<violation number="1" location="optional-mcps/unstructured-foundation/manifest.yaml:51">
P3: The post-install note hardcodes a token cache path `~/.hermes/mcp-tokens/`. Since Hermes is profile-aware (HERMES_HOME can differ), this literal path may not apply in all setups. Consider a generic phrasing like "Tokens are cached automatically and refreshed." to avoid misleading users with non-default profiles.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
|
|
||
| On first connection, Hermes opens a browser to authenticate with Foundation | ||
| (OAuth 2.1). Tokens are cached under ~/.hermes/mcp-tokens/ and refreshed | ||
| automatically. |
There was a problem hiding this comment.
P3: The post-install note hardcodes a token cache path ~/.hermes/mcp-tokens/. Since Hermes is profile-aware (HERMES_HOME can differ), this literal path may not apply in all setups. Consider a generic phrasing like "Tokens are cached automatically and refreshed." to avoid misleading users with non-default profiles.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optional-mcps/unstructured-foundation/manifest.yaml, line 51:
<comment>The post-install note hardcodes a token cache path `~/.hermes/mcp-tokens/`. Since Hermes is profile-aware (HERMES_HOME can differ), this literal path may not apply in all setups. Consider a generic phrasing like "Tokens are cached automatically and refreshed." to avoid misleading users with non-default profiles.</comment>
<file context>
@@ -0,0 +1,69 @@
+
+ On first connection, Hermes opens a browser to authenticate with Foundation
+ (OAuth 2.1). Tokens are cached under ~/.hermes/mcp-tokens/ and refreshed
+ automatically.
+
+ Two things to know because auth happens on first connect:
</file context>
…ort (NousResearch#61734) test_accepted_at_every_position spawned 11 separate 'python -m hermes_cli.main' subprocesses, each cold-importing the full CLI module tree under a 15s TimeoutExpired deadline. On a loaded CI worker the import alone can exceed that (slice 2/8 flaked exactly here on PR NousResearch#61726's run, TimeoutExpired at subprocess.py:1253), failing PRs that never touched the CLI. Replace with ONE driver subprocess that imports hermes_cli.main once and parses all 11 argvs in-process (catching SystemExit per argv), reporting JSON results. Same assertions per argv, identical semantics (verified the --help-before-unknown-flag exit behavior matches the old method), ~11x less import work, and the 180s timeout only trips on a genuine hang.
…anscript When gateway session-hygiene auto-compression fires with in-place compaction, the flow was: 1. _compress_context() calls archive_and_compact() — soft-archives old rows (active=0, compacted=1) and inserts compacted messages as the new active set. This is the non-destructive, durable path. 2. The hygiene handler then called rewrite_transcript() — which calls replace_messages(active_only=False) — DELETEing ALL rows including the just-archived turns. Silent permanent data loss (NousResearch#61145). The interactive /compress handler had the same bug. Fix: only call rewrite_transcript() when session rotation produced a new session id (legacy path). When in-place compaction succeeded, skip the rewrite — archive_and_compact() already handled persistence. Closes NousResearch#61145.
Flip the two tests that pinned the old buggy behavior (rewrite_transcript called after in-place compaction) to assert the corrected invariant from NousResearch#61145: archive_and_compact() already persisted, so the handler must NOT call rewrite_transcript — its replace_messages(active_only=False) would DELETE the just-archived rows. E2E-verified against a real SessionDB: 6 soft-archived rows are wiped by replace_messages' default path, confirming the data-loss premise.
His noreply email has no numeric-id+ prefix, so the attribution CI's auto-resolve pattern doesn't match it.
Every MemoryStore instance opened its own SQLite connection guarded by its own RLock. Several providers coexist in one process (the main agent plus every delegate_task subagent), so instances pointing at the same memory_store.db raced as independent WAL writers. Combined with writes that were not rolled back on error, one connection could leave an open write transaction that pinned the write lock and made every other connection's writes fail with "database is locked" for the full busy timeout. Instances for the same database now share ONE process-wide connection and ONE re-entrant lock, so access is fully serialized and cross-connection contention is impossible. The shared connection is refcounted: closing one instance never tears it out from under a live sibling, and the last close releases it. The connection runs in autocommit (isolation_level=None) so a write that raises mid-method can never leave a dangling transaction holding the write lock; the existing explicit commit() calls become harmless no-ops. The provider's shutdown() now calls the refcount-guarded close() instead of just dropping the reference: leaving finalization to GC kept the connection (and its write lock) alive indefinitely on long-running gateways, prolonging the exact contention this fix removes. The last provider now releases the connection deterministically while siblings stay live; regression tests fail without the wiring. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-ups for salvaged PR NousResearch#43819: the registry key was str(Path(db_path).expanduser()) — a symlinked or relative path to the same DB file got its own connection, silently reintroducing the exact multi-writer contention the registry prevents. Key on Path.resolve() (OSError-tolerant fallback). Adds a symlink regression test and the AUTHOR_MAP entry for adambiggs.
switch_model() rebuilds _client_kwargs from scratch (api_key + base_url) but does not call _apply_client_headers_for_base_url(), so provider- specific headers like OpenRouter HTTP-Referer and X-Title are lost. Subsequent requests show "Unknown" in OpenRouter dashboard logs. Call _apply_client_headers_for_base_url() after rebuilding _client_kwargs and before creating the new client. Fixes NousResearch#61099
Three tests for the NousResearch#61099 salvage: OpenRouter attribution headers present after switching to openrouter.ai, Kimi User-Agent sentinel present after switching to api.kimi.com, and stale headers cleared when switching to a provider with no URL-specific headers. 2/3 fail on unpatched main (DID NOT ATTACH), confirming the bug.
Sibling site of the load_cli_config fix (NousResearch#58277): _deep_merge treated a YAML-null section (terminal: with no value) as an override, replacing the entire DEFAULT_CONFIG dict for that section with None. Every downstream consumer expecting a mapping was a latent crash, and default sub-keys were silently lost. A None override of a dict default is now ignored, matching the CLI loader's behavior. Scalar-null overrides are unchanged.
GatewayConfig.from_dict(), PlatformConfig.from_dict(), SessionResetPolicy.from_dict(), and StreamingConfig.from_dict() assumed their input sections were mappings. A malformed scalar from legacy gateway.json or an internal caller could crash config loading with AttributeError before env overrides/defaults had a chance to recover. Coerce non-mapping sections to empty dicts, skip malformed platform entries, and keep valid sibling platform configs loading normally. Tests cover scalar platform blocks, scalar nested reset/streaming sections, and malformed PlatformConfig home_channel/extra values. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
The streaming fallback path read yaml_cfg.get("gateway", {}).get("streaming") when top-level streaming was absent or malformed. If a user accidentally set gateway to a scalar value, config loading crashed with AttributeError instead of ignoring the malformed block and using defaults.
Read the gateway block once, verify it is a mapping before accessing nested streaming, and keep the existing gateway.platforms fallback using the same checked value.
Adds a regression test for config.yaml containing gateway: disabled.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
Curator review forks now pass credential_pool and request_overrides from resolve_runtime_provider into AIAgent so pool-backed custom providers can rotate credentials on 401 like main chat.
Regression test that _run_llm_review passes credential_pool and request_overrides from resolve_runtime_provider into the curator AIAgent fork.
When web_search results are passed directly to web_extract, the URLs
field contains dict objects (e.g., {"url": "...", "title": "..."})
rather than plain URL strings. Two code paths assumed URLs were always
strings and crashed:
- agent/display.py get_cute_tool_message for web_extract: tried to call
url.replace() on a dict, causing AttributeError
- tools/web_tools.py web_extract_tool loop: tried regex search on a dict,
causing TypeError
Both now extract the URL string from dict objects (url or href field) or
fall back to empty string, preserving the cosmetic display and allowing
the tool to process the URLs correctly.
Fixes NousResearch#61693
get_due_jobs()'s one-shot stale-entry recovery (NousResearch#38758) treated an expired run_claim (NousResearch#59229) as proof the claiming tick died, but a run stalled on network I/O — or a laptop asleep mid-run — legitimately outlives the TTL while very much alive. The recovery then deleted the job record mid-flight: list showed the job gone, and when the run finished mark_job_run() found nothing to update, so last_run_at / last_status / last_delivery_error were never recorded. Two guards, per the liveness signals available: - Same process (the common single-gateway case): before removing a dispatch-limit-reached one-shot, consult the scheduler's running set via a lazy import; if the job is still running here it is slow, not stale — keep the entry. - Cross process: run_job's monitor loop now refreshes run_claim.at every 60s while the run is alive (including under HERMES_CRON_TIMEOUT=0, which previously blocked without polling), so an expired claim really does mean the owner died and the TTL stays a dead-owner detector. Fixes NousResearch#62002
…rch#61979) force_close_tcp_sockets stayed shutdown-only after NousResearch#29507 to avoid cross-thread FD recycle. That left CLOSED sockets unreclaimed when httpx.close() skipped already-shutdown sockets under long-lived gateways (~1 CLOSED fd / 6 min via proxy). Add release_fds= for the owning-thread dispose path only; abort still defaults to shutdown-only.
…s chat finalize passed conversation_history=history aliasing the snapshot so flush skipped every message and wrote nothing. now flush _session_messages via marker dedup like gateway shutdown. add real db e2e tests.
…1793) * feat(security): expose deterministic tool output risk * fix(security): emit output-risk events only for findings
removing tsc -b from the build script (previous commit) also removed the only step that type-checked the electron/ directory — the CI typecheck job runs tsc -p . --noEmit, which uses tsconfig.json whose include is only ["src", "../shared/src"], so electron/ was silently uncovered. extend the typecheck script to also run against tsconfig.electron.json so electron/ stays type-checked in CI.
Adds the Unstructured Transform hosted MCP server to the curated catalog as the `unstructured` entry. Transform turns any document into structured, AI-ready data on demand — partitioning, enriching, chunking, and embedding across 60+ formats (PDFs, emails, images, scanned docs). It drops document parsing/extraction into any Hermes workflow (reading spec PDFs while coding, extracting structure from scanned contracts, chunking + embedding policy docs for grounding) without a bespoke ETL pipeline. Follows the `linear` archetype: remote Streamable HTTP + native MCP OAuth 2.1 (case 1, no third-party provider). Hermes's mcp_oauth_manager handles DCR, PKCE, token exchange, and refresh; nothing installs locally. The npx `mcp-remote` bridge in Unstructured's docs is only for clients without native remote-MCP+OAuth support, so Hermes connects to the URL directly. - optional-mcps/unstructured/manifest.yaml — new http+oauth entry (url https://mcp.transform.unstructured.io). `tools.default_enabled` left unset so the install-time probe lists the live tool surface (matches linear). post_install covers the free tier (15k pages/month, no approval) and the OAuth-on-first-connect pitfalls (probe-before-auth, in-session reload race, headless/SSH). - pyproject.toml — per-entry [tool.setuptools.data-files] target so the manifest ships in the wheel (matches linear/n8n; graft optional-mcps in MANIFEST.in already covers the sdist). Validation: test_mcp_catalog.py + test_packaging_metadata.py (46 passed); `hermes mcp catalog` lists the entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6969ae9 to
7cdd3ba
Compare
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Addresses PR review: the post_install note hardcoded ~/.hermes/mcp-tokens/, but tools/mcp_oauth.py:_get_token_dir() derives the path from get_hermes_home() (HERMES_HOME), so it's HERMES_HOME/mcp-tokens/ per profile. Reword to reference the active Hermes home, keeping ~/.hermes/mcp-tokens/ only as the default-profile aside. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
1 issue found across 1 file (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="optional-mcps/unstructured/manifest.yaml">
<violation number="1" location="optional-mcps/unstructured/manifest.yaml:53">
P3: Windows users are given the wrong default token location: the default Hermes home is `%LOCALAPPDATA%/hermes`, not `~/.hermes`. Qualify this path as Unix-like or include the Windows default.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
Fix all with cubic | Re-trigger cubic
| (OAuth 2.1) — sign in with your Unstructured account. Transform includes a | ||
| free tier (15,000 pages/month), so no special approval is needed to start. | ||
| Tokens are cached under your active Hermes home (HERMES_HOME/mcp-tokens/, | ||
| which is ~/.hermes/mcp-tokens/ on the default profile) and refreshed |
There was a problem hiding this comment.
P3: Windows users are given the wrong default token location: the default Hermes home is %LOCALAPPDATA%/hermes, not ~/.hermes. Qualify this path as Unix-like or include the Windows default.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At optional-mcps/unstructured/manifest.yaml, line 53:
<comment>Windows users are given the wrong default token location: the default Hermes home is `%LOCALAPPDATA%/hermes`, not `~/.hermes`. Qualify this path as Unix-like or include the Windows default.</comment>
<file context>
@@ -49,7 +49,9 @@ post_install: |
free tier (15,000 pages/month), so no special approval is needed to start.
- Tokens are cached under ~/.hermes/mcp-tokens/ and refreshed automatically.
+ Tokens are cached under your active Hermes home (HERMES_HOME/mcp-tokens/,
+ which is ~/.hermes/mcp-tokens/ on the default profile) and refreshed
+ automatically.
</file context>
| which is ~/.hermes/mcp-tokens/ on the default profile) and refreshed | |
| which defaults to ~/.hermes/mcp-tokens/ on Unix-like platforms) and refreshed |
Summary
Adds the Unstructured Transform hosted MCP server to the Nous-approved MCP catalog (
optional-mcps/), as catalog entryunstructured.Transform turns any document into structured, AI-ready data on demand — it partitions, enriches, chunks, and embeds files across 60+ formats (PDFs, emails, images, scanned docs, …). It drops document parsing/extraction into any Hermes workflow without a bespoke ETL pipeline: reading a spec PDF while coding, extracting structure from scanned contracts, chunking + embedding a policy doc for grounding, turning an email thread into structured records. You describe the processing in plain language; Transform handles extraction and parsing.
Design
Follows the
lineararchetype — remote Streamable HTTP + native MCP OAuth 2.1 (case 1, no third-partyprovider). Hermes'smcp_oauth_managerhandles discovery, dynamic client registration (RFC 7591), PKCE, token exchange, and refresh. Nothing installs locally.docs.unstructured.io/transform/*. Thenpx -y mcp-remote https://mcp.transform.unstructured.iocommand in Unstructured's docs is a stdio bridge for clients that lack native remote-MCP+OAuth support — Hermes connects to the URL directly (same aslinear), so no bridge is needed.401unauthenticated, so the SDK's reactive OAuth trigger fires correctly — this is not one of the auth-optional / DCR-less servers (Google Drive, Atlassian, Blynkhermes mcp loginnever triggers OAuth when the MCP server allowsinitializewithout auth (e.g. Blynk) NousResearch/hermes-agent#53870) that need a pre-registered client.Why
tools.default_enabledis unsetThe exact tool names aren't pinned in a stable public doc, so the install-time probe lists whatever the live server exposes, all pre-checked — the same choice the
linearentry made. A follow-up PR can encode a curated read-only subset once the surface is documented.Files
optional-mcps/unstructured/manifest.yaml— new http+oauth entry.post_installcovers the free tier (15,000 pages/month, no approval needed to start) and the OAuth-on-first-connect pitfalls surfaced in upstream issues:hermes mcp configure unstructured). Matches the documented probe-fail fallback (MCP OAuth flow times out at 40s — _probe_single_server default connect_timeout too short for interactive OAuth NousResearch/hermes-agent#50485).hermes mcp login(the in-session config reload only waits ~30s).pyproject.toml— per-entry[tool.setuptools.data-files]target so the manifest actually ships in the wheel (matcheslinear/n8n). Without it the entry parses in-repo but vanishes from packaged installs — the exact bug open PR fix(packaging): ship unreal-engine MCP manifest in the wheel NousResearch/hermes-agent#52108 fixes forunreal-engine.graft optional-mcpsinMANIFEST.inalready covers the sdist.Validation
tests/hermes_cli/test_mcp_catalog.py(incl.test_all_shipped_manifests_parse)tests/test_packaging_metadata.pyhermes mcp catalog(live)unstructured, column aligned_parse_manifest)https://mcp.transform.unstructured.io, provider=None, default_enabled=None, 0 diagnostics🤖 Generated with Claude Code