Skip to content

feat(mcp-catalog): add Unstructured Transform MCP server#1

Open
ctrahey wants to merge 125 commits into
mainfrom
feat/mcp-catalog-unstructured-foundation
Open

feat(mcp-catalog): add Unstructured Transform MCP server#1
ctrahey wants to merge 125 commits into
mainfrom
feat/mcp-catalog-unstructured-foundation

Conversation

@ctrahey

@ctrahey ctrahey commented Jul 10, 2026

Copy link
Copy Markdown

Summary

Adds the Unstructured Transform hosted MCP server to the Nous-approved MCP catalog (optional-mcps/), as catalog entry unstructured.

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.

ℹ️ Draft — raised inside the fork. Base Unstructured-IO/hermes-agent:main, not upstream, until we're ready to submit to Nous.

Design

Follows the linear archetype — remote Streamable HTTP + native MCP OAuth 2.1 (case 1, no third-party provider). Hermes's mcp_oauth_manager handles discovery, dynamic client registration (RFC 7591), PKCE, token exchange, and refresh. Nothing installs locally.

transport:
  type: http
  url: https://mcp.transform.unstructured.io
auth:
  type: oauth
  • Endpoint confirmed against docs.unstructured.io/transform/*. The npx -y mcp-remote https://mcp.transform.unstructured.io command in Unstructured's docs is a stdio bridge for clients that lack native remote-MCP+OAuth support — Hermes connects to the URL directly (same as linear), so no bridge is needed.
  • Transform's server returns 401 unauthenticated, so the SDK's reactive OAuth trigger fires correctly — this is not one of the auth-optional / DCR-less servers (Google Drive, Atlassian, Blynk hermes mcp login never triggers OAuth when the MCP server allows initialize without auth (e.g. Blynk) NousResearch/hermes-agent#53870) that need a pre-registered client.

Why tools.default_enabled is unset

The 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 linear entry made. A follow-up PR can encode a curated read-only subset once the surface is documented.

Files

Validation

Check Result
tests/hermes_cli/test_mcp_catalog.py (incl. test_all_shipped_manifests_parse) pass
tests/test_packaging_metadata.py pass (46 passed across both files)
hermes mcp catalog (live) lists unstructured, column aligned
Manifest schema parse (_parse_manifest) http/oauth, url https://mcp.transform.unstructured.io, provider=None, default_enabled=None, 0 diagnostics

🤖 Generated with Claude Code

giggling-ginger and others added 16 commits July 8, 2026 07:34
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.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@cubic-dev-ai cubic-dev-ai Bot Jul 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Fix with cubic

teknium1 and others added 13 commits July 9, 2026 17:56
…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>
kshitijk4poor and others added 26 commits July 10, 2026 17:50
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>
@ctrahey
ctrahey force-pushed the feat/mcp-catalog-unstructured-foundation branch from 6969ae9 to 7cdd3ba Compare July 10, 2026 18:42
@socket-security

socket-security Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updatedpypi/​lark-oapi@​1.5.3 ⏵ 1.6.898100100100100

View full report

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>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@cubic-dev-ai cubic-dev-ai Bot Jul 13, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
which is ~/.hermes/mcp-tokens/ on the default profile) and refreshed
which defaults to ~/.hermes/mcp-tokens/ on Unix-like platforms) and refreshed
Fix with cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.