From 03e6db193b903e088ed8f9e7920f81e3e3f32846 Mon Sep 17 00:00:00 2001 From: OptimNow Date: Thu, 20 Aug 2026 16:58:59 +0200 Subject: [PATCH 1/2] Hash the root connector URL for ui.domain - the host sees the root form The 2026-08-20 Desktop test after the Skybridge-parity merge moved the failure one step forward: mcp-ext-apps-host now engages and logs 'ui.domain validation failed for connector "https://cloud-finops-skills-590a051d.alpic.live/"' - the ROOT url with its trailing slash, while UI_DOMAIN hashed '/mcp'. Skybridge, whose connectors render, hashes https:// per request, which for a root-entered connector is exactly the root-with-slash form. Alpic serves the MCP at the root as well as /mcp (verified by a live initialize), so: hash the root form, document the root form as THE connector URL in README and INSTALLATION.md (a variant connects fine but silently disables widget rendering), and correct the wrong 'no trailing slash' note in the CLAUDE.md lesson. --- CLAUDE.md | 9 +++++++-- INSTALLATION.md | 9 ++++++--- README.md | 4 ++-- mcp_server/src/cloud_finops_mcp/server.py | 24 ++++++++++++++--------- mcp_server/tests/test_ui.py | 16 ++++++++------- 5 files changed, 39 insertions(+), 23 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 8cc412c..bbb5369 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -386,8 +386,13 @@ maintainer's control and on an unpublished timeline. **Update (2026-08-19): the gate now has an observable mechanism.** Claude Desktop's `mcp-ext-apps-host` attempts to set up MCP Apps for custom connectors and validates a `ui.domain` on the widget resource: -sha256(connector URL as entered in Settings, no trailing slash)[:32] + -`.claudemcpcontent.com`. A wrong or missing value logs +sha256(connector URL exactly as the host displays it in its error log - +for a root-entered connector that is the ROOT url WITH its trailing +slash)[:32] + `.claudemcpcontent.com`. (Corrected 2026-08-20: the +original "no trailing slash" note here was wrong - hashing `/mcp` while +the host saw `https://.../` reproduced the failure on this repo's own +connector; Skybridge, which passes, hashes https:// per +request.) A wrong or missing value logs `ui.domain validation failed for connector ""` in the Desktop logs (observed live for ai-pricing-hub on 2026-08-19, including the fix-it one-liner the error prints). So the 2026-08-16 claim that a custom diff --git a/INSTALLATION.md b/INSTALLATION.md index 12ea01e..525a5d5 100644 --- a/INSTALLATION.md +++ b/INSTALLATION.md @@ -293,12 +293,15 @@ There are two ways to reach it. #### Hosted (nothing to install) ```bash -claude mcp add --transport http cloud-finops https://cloud-finops-skills-590a051d.alpic.live/mcp +claude mcp add --transport http cloud-finops https://cloud-finops-skills-590a051d.alpic.live/ ``` For Claude.ai / Claude Desktop, **Settings -> Connectors -> Add custom connector** and -paste the same URL. Cursor, Windsurf, VS Code and ChatGPT take an HTTP MCP server entry -pointing at it. +paste exactly `https://cloud-finops-skills-590a051d.alpic.live/` - trailing slash +included. The widget sandbox domain (MCP Apps) is derived from the URL as entered, so +a variant form (`/mcp`, no slash) connects fine but silently disables widget rendering. +Cursor, Windsurf, VS Code and ChatGPT take an HTTP MCP server entry pointing at the +same URL. This is the same code as the package below, deployed on Alpic and tracking `main`. Prefer it unless you need the server to run offline or pinned to a version. diff --git a/README.md b/README.md index 60e5f23..11d9bda 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ | Gemini | Self-host: `./install.sh --tool gemini` _(a public Cloud FinOps Gem is on the Roadmap)_ | | Cursor Windsurf Codex Aider Copilot Kiro IDE Gemini CLI | One-liner: `curl -sL https://raw.githubusercontent.com/OptimNow/cloud-finops-skills/main/install.sh \| bash -s -- --tool ` | | Auto-detect | `curl -sL https://raw.githubusercontent.com/OptimNow/cloud-finops-skills/main/install.sh \| bash` | -| MCP hosted | Nothing to install: `claude mcp add --transport http cloud-finops https://cloud-finops-skills-590a051d.alpic.live/mcp`. For Claude.ai / Desktop, **Settings -> Connectors -> Add custom connector** with the same URL | +| MCP hosted | Nothing to install: `claude mcp add --transport http cloud-finops https://cloud-finops-skills-590a051d.alpic.live/`. For Claude.ai / Desktop, **Settings -> Connectors -> Add custom connector** with exactly that URL (trailing slash included - the widget sandbox domain is derived from it) | | MCP package | `pip install cloud-finops-mcp` then add to your MCP client config (Claude Code / Cursor / Codex / Windsurf / Cline). Snippets: `./install.sh --tool mcp`. Six tools - faceted retrieval over the reference library and named-pattern playbooks. | Full options, troubleshooting, and the model-agnostic API loader: see [INSTALLATION.md](./INSTALLATION.md). @@ -383,7 +383,7 @@ For agents that want tool-style retrieval rather than full-context injection, th is also an MCP server. Hosted, or as a PyPI package: ```bash -claude mcp add --transport http cloud-finops https://cloud-finops-skills-590a051d.alpic.live/mcp +claude mcp add --transport http cloud-finops https://cloud-finops-skills-590a051d.alpic.live/ ``` ```bash diff --git a/mcp_server/src/cloud_finops_mcp/server.py b/mcp_server/src/cloud_finops_mcp/server.py index 2a31e14..c0027a6 100644 --- a/mcp_server/src/cloud_finops_mcp/server.py +++ b/mcp_server/src/cloud_finops_mcp/server.py @@ -54,16 +54,22 @@ # The claude.ai / Claude Desktop MCP Apps host only mounts a widget iframe # when the ui:// resource declares the sandbox domain it will be served from: -# sha256()[:32] + ".claudemcpcontent.com". A wrong or missing value -# logs "ui.domain validation failed for connector " in the host log -# (mcp-ext-apps-host) and leaves a dead blank frame - observed live on -# 2026-08-19 for both this server and ai-pricing-hub. The hash is DERIVED -# from the canonical URL rather than pasted, so the URL constant is the only -# thing that has to stay true; hashing an internal path instead of the -# public URL is exactly the mistake that broke ai-pricing-hub. +# sha256()[:32] + ".claudemcpcontent.com". A wrong or +# missing value logs 'ui.domain validation failed for connector ""' +# in the Desktop log (mcp-ext-apps-host) and leaves a dead blank frame. +# Observed live on 2026-08-20 for THIS server: the host quoted the +# connector as "https://cloud-finops-skills-590a051d.alpic.live/" - the +# ROOT url with its trailing slash - while the hash here was computed over +# "/mcp". Skybridge, whose connectors render, hashes +# https:// per request, which for a root-entered +# connector is exactly the root-with-slash form. Alpic serves the MCP at +# the root as well as /mcp, so the documented connector URL (README, +# INSTALLATION.md) is the root form and this constant must match it +# byte-for-byte; hashing an internal path or the wrong variant is exactly +# the mistake that broke ai-pricing-hub and then this server. CANONICAL_CONNECTOR_ORIGIN = "https://cloud-finops-skills-590a051d.alpic.live" -CANONICAL_CONNECTOR_URL = CANONICAL_CONNECTOR_ORIGIN + "/mcp" +CANONICAL_CONNECTOR_URL = CANONICAL_CONNECTOR_ORIGIN + "/" UI_DOMAIN = ( hashlib.sha256(CANONICAL_CONNECTOR_URL.encode("utf-8")).hexdigest()[:32] + ".claudemcpcontent.com" diff --git a/mcp_server/tests/test_ui.py b/mcp_server/tests/test_ui.py index d2a2a4e..2263356 100644 --- a/mcp_server/tests/test_ui.py +++ b/mcp_server/tests/test_ui.py @@ -153,16 +153,18 @@ async def test_tools_link_their_widgets() -> None: def test_ui_domain_is_derived_from_the_canonical_connector_url() -> None: """The sandbox-domain hash must follow the URL constant, never drift. - The claude host validates sha256()[:32] + ".claudemcpcontent.com" against the resource meta; a - pasted hash that stops matching the URL is the ai-pricing-hub failure - mode. + The claude host validates sha256()[:32] + ".claudemcpcontent.com" against the resource meta. + Observed live on 2026-08-20: hashing "/mcp" while the host saw the + root url logged 'ui.domain validation failed'. Skybridge hashes + https://, i.e. root-with-slash. """ import hashlib - assert not server.CANONICAL_CONNECTOR_URL.endswith("/"), ( - "the connector URL is hashed as entered - no trailing slash" - ) + assert server.CANONICAL_CONNECTOR_URL == ( + server.CANONICAL_CONNECTOR_ORIGIN + "/" + ), "the connector URL is the root form, trailing slash included" expected = ( hashlib.sha256(server.CANONICAL_CONNECTOR_URL.encode("utf-8")).hexdigest()[:32] + ".claudemcpcontent.com" From 39886853a18df10514ca488ba56c63111d39d7c7 Mon Sep 17 00:00:00 2001 From: OptimNow Date: Thu, 20 Aug 2026 17:04:34 +0200 Subject: [PATCH 2/2] Add 'Skill or MCP, or both?' guidance and practitioner-language tool surface The tool titles and descriptions answered repo mechanics ('List every bundled named-pattern playbook') rather than FinOps intent - a practitioner who has never seen the repo does not ask for 'playbooks', they ask which VMs run for nothing. Field testing also showed the model routes advisory questions to tools it can read an intent match for. - Six tool titles and docstring openings rewritten around practitioner questions; parameters, returns and semantics unchanged (all three copies moved together: server.py, README.md, mcp_server/README.md). - New README section 'Skill or MCP, or both?' with the field-tested behaviour difference (skill grounds advisory answers, connector wins at lookups and distribution) and the recommended setup per situation. - get_reference's description now tells the agent to fetch BEFORE answering advisory questions - the strongest routing lever available at the tool layer. --- README.md | 39 +++++++++++-- mcp_server/README.md | 12 ++-- mcp_server/src/cloud_finops_mcp/server.py | 67 ++++++++++++++--------- 3 files changed, 80 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index 11d9bda..ea0d03b 100644 --- a/README.md +++ b/README.md @@ -377,6 +377,33 @@ A version-tagged release zip (`cloud-finops-vX.Y.Z.zip`) is attached to every [GitHub release](https://github.com/OptimNow/cloud-finops-skills/releases) for Claude Desktop / claude.ai users who prefer downloading over building locally. +### Skill or MCP, or both? + +Same content, two delivery mechanisms - and they behave differently, because of +how models use them. Field-tested conclusion (same battery of practitioner +questions through both channels): + +- **The skill is pushed into context.** The guidance is already there when the + model reasons, so it grounds *advisory* answers - commitment sizing, + chargeback design, allocation methodology - without the model having to + decide anything. This is the primary channel on surfaces that support + skills (Claude Code, Claude Desktop, claude.ai, Kiro). +- **The MCP server is retrieval on demand.** The model must decide to call a + tool, which it reliably does for lookup questions ("show me the idle waste + runbooks") and much less for advisory ones, where it tends to answer from + its own knowledge. Its strengths are distribution (paste one URL, nothing + to install - the right path for non-technical users and for hosts without + skill support), faceted queries over the library's metadata, and + interactive widgets on hosts that render MCP Apps. +- **Recommended setup on Claude:** install the skill, and add the + [AI Pricing Hub](https://optimtoken.optimnow.io) connector next to it - the + skill carries the doctrine, the hub serves live prices when the skill + routes a pricing question to it. Add the cloud-finops connector too if you + want the widgets or work across hosts; skip it if the skill is already + loaded and you only need text answers. +- **No skill support in your tool?** Use the MCP server alone - it is the + same library behind six read-only tools. + ### MCP server (cross-tool, search-style retrieval) For agents that want tool-style retrieval rather than full-context injection, the skill @@ -400,18 +427,18 @@ patterns. | Tool | What it answers | |---|---| -| `list_references()` | What is in the library? Every reference with its FinOps Framework metadata | -| `get_reference(name)` | Give me the full text of one | -| `find_references(domain?, capability?, phase?, persona?, maturity?, persona_primary_only?)` | Which references serve Rate Optimization, at Walk maturity, written for Engineering? (`persona_primary_only` cuts to the primary audience - broad personas collaborate on nearly everything) | +| `list_references()` | What guidance exists? The full catalogue with its FinOps Framework metadata | +| `get_reference(name)` | The full guide on one topic - mechanics, decision rules, worked examples | +| `find_references(domain?, capability?, phase?, persona?, maturity?, persona_primary_only?)` | "How should we size Savings Plans?" "What must be true before chargeback?" - routes a FinOps question to the guides that serve it (`persona_primary_only` cuts to the primary audience) | **Playbooks** - small named-pattern runbooks, one waste pattern each. Reach for these for "how do I detect and fix this specific thing". | Tool | What it answers | |---|---| -| `list_playbooks()` | Which named patterns exist? | -| `get_playbook(name)` | The runbook: symptoms, detection query, fix, anti-pattern | -| `find_playbooks(scope?, service?, waste_category?, confidence?)` | Which AWS playbooks cover orphaned resources at obvious confidence? | +| `list_playbooks()` | What cloud waste can we hunt with a ready-made runbook? | +| `get_playbook(name)` | The step-by-step runbook: symptoms, detection queries, fix, anti-pattern | +| `find_playbooks(scope?, service?, waste_category?, confidence?)` | "Which VMs run for nothing?" "Why is the NAT bill so high?" - finds the runbook for a specific waste suspicion | The faceted queries are the reason this is a server and not just a folder of markdown. Every file carries YAML frontmatter mapping it to a FinOps Framework Capability, phase, diff --git a/mcp_server/README.md b/mcp_server/README.md index 092582c..52443ef 100644 --- a/mcp_server/README.md +++ b/mcp_server/README.md @@ -19,9 +19,9 @@ Six tools, all read-only, split across two surfaces. | Tool | Purpose | |---|---| -| `list_references()` | List all references with their FCP metadata. | -| `get_reference(name)` | Fetch the full markdown body of one reference. | -| `find_references(domain?, capability?, phase?, persona?, maturity?, persona_primary_only?)` | Faceted query over the FinOps Capability/Phase frontmatter. | +| `list_references()` | Browse the knowledge library: what guidance exists, with its FinOps Framework metadata. | +| `get_reference(name)` | Read the full guide on one topic - mechanics, decision rules, worked examples. | +| `find_references(domain?, capability?, phase?, persona?, maturity?, persona_primary_only?)` | Route a FinOps question (commitment sizing, chargeback design, ...) to the guides that serve it, by FinOps Framework facet. | The reference faceted query supports any combination of: @@ -38,9 +38,9 @@ The reference faceted query supports any combination of: | Tool | Purpose | |---|---| -| `list_playbooks()` | List all named-pattern playbooks with their metadata. | -| `get_playbook(name)` | Fetch the full markdown body of one playbook. | -| `find_playbooks(scope?, service?, waste_category?, confidence?)` | Faceted query over the playbook frontmatter. | +| `list_playbooks()` | Browse the waste runbooks: which patterns of idle, orphaned, overprovisioned or leaking spend have a ready-made runbook. | +| `get_playbook(name)` | Read one runbook: symptoms, detection queries, fix, anti-pattern. | +| `find_playbooks(scope?, service?, waste_category?, confidence?)` | "We are wasting money on X - how do I find and fix it?" - filter runbooks by provider, service, waste category, confidence. | The playbook faceted query supports: diff --git a/mcp_server/src/cloud_finops_mcp/server.py b/mcp_server/src/cloud_finops_mcp/server.py index c0027a6..0a82d84 100644 --- a/mcp_server/src/cloud_finops_mcp/server.py +++ b/mcp_server/src/cloud_finops_mcp/server.py @@ -175,17 +175,19 @@ def _load_ui(filename: str) -> str: @mcp.tool( - title="List FinOps references", + title="Browse the FinOps knowledge library", annotations=_READ_ONLY, meta=_ui_tool_meta(REFERENCE_BROWSER_URI), ) def list_references() -> dict[str, Any]: - """List every bundled FinOps reference with its FCP metadata. + """See what FinOps guidance is available: billing mechanics, commitment + strategy, allocation and chargeback, AI cost management, and per-provider + cost handbooks (AWS, Azure, GCP, OCI, Databricks, Snowflake, ...). - Use this to discover what the reference library covers before deciding - what to fetch. When the question already names a FinOps domain, phase, - persona or maturity, call ``find_references`` instead of scanning this - full list. + Use this to discover what the library covers before deciding what to + fetch. When the question already names a FinOps domain, phase, persona + or maturity, call ``find_references`` instead of scanning this full + list. Returns a dict shaped ``{"references": [...], "total": N}`` where each entry includes ``name``, ``description``, FCP fields (``fcp_domain``, @@ -194,13 +196,16 @@ def list_references() -> dict[str, Any]: return _tools.list_references() -@mcp.tool(title="Get a FinOps reference", annotations=_READ_ONLY) +@mcp.tool(title="Read one FinOps guide", annotations=_READ_ONLY) def get_reference(name: str) -> dict[str, Any]: - """Fetch the full markdown content of one reference by name. + """Fetch the full guidance on one FinOps topic - the billing mechanics, + decision rules and worked examples behind a defensible answer. - Use this when you need the actual billing-mechanics content of one - known reference - after ``list_references`` or ``find_references`` told - you which one serves the question. + Use this when you need the actual content of one known reference - + after ``list_references`` or ``find_references`` told you which one + serves the question, and ALWAYS before answering an advisory question + (commitment sizing, chargeback design, allocation methodology) the + library covers. Args: name: Reference name as returned by ``list_references`` (e.g. @@ -215,7 +220,7 @@ def get_reference(name: str) -> dict[str, Any]: @mcp.tool( - title="Find FinOps references by facet", + title="Find the right FinOps guide", annotations=_READ_ONLY, meta=_ui_tool_meta(REFERENCE_BROWSER_URI), ) @@ -227,10 +232,13 @@ def find_references( maturity: str | None = None, persona_primary_only: bool = False, ) -> dict[str, Any]: - """Filter references by FinOps Capability/Phase (FCP) frontmatter. + """Find which guidance serves a FinOps question - how to commit, size, + allocate, charge back, forecast, or govern cloud and AI spend. - Use this when the question maps to FinOps Framework facets - a domain, - capability, phase, persona, or maturity stage - and you want only the + Use this for questions like "how should we size Savings Plans", + "what should Finance own in chargeback", "what does a Crawl-stage org + tackle first" - anything that maps to FinOps Framework facets (domain, + capability, phase, persona, maturity) - and you want only the references that serve it, instead of scanning the full list. All filters are optional and combine with AND semantics. String matching @@ -274,14 +282,16 @@ def find_references( @mcp.tool( - title="List waste playbooks", + title="Browse the cloud waste runbooks", annotations=_READ_ONLY, meta=_ui_tool_meta(PLAYBOOK_EXPLORER_URI), ) def list_playbooks() -> dict[str, Any]: - """List every bundled named-pattern playbook. + """See every ready-made runbook for finding and fixing cloud waste: + idle, orphaned and overprovisioned resources, egress surprises, + schedule blindness and AI/ML inefficiency across AWS, Azure and GCP. - Use this to discover which named waste patterns exist. When the + Use this to discover which waste patterns have a runbook. When the question already names a provider, waste category, or confidence tier, call ``find_playbooks`` instead. @@ -296,12 +306,14 @@ def list_playbooks() -> dict[str, Any]: @mcp.tool( - title="Get a waste playbook", + title="Read one waste runbook", annotations=_READ_ONLY, meta=_ui_tool_meta(PLAYBOOK_VIEWER_URI), ) def get_playbook(name: str) -> dict[str, Any]: - """Fetch the full markdown content of one playbook by slug. + """Fetch the step-by-step runbook for one specific waste pattern: + symptoms, the detection queries to run, the fix, and the anti-pattern + to avoid. Use this when the user asks how to detect, confirm, or fix one specific named waste pattern (zombie NAT gateway, snapshot sprawl, idle SageMaker @@ -417,7 +429,7 @@ def reference_browser_ui_apps_sdk() -> str: @mcp.tool( - title="Find waste playbooks by facet", + title="Find a waste runbook", annotations=_READ_ONLY, meta=_ui_tool_meta(PLAYBOOK_EXPLORER_URI), ) @@ -427,11 +439,14 @@ def find_playbooks( waste_category: str | None = None, confidence: str | None = None, ) -> dict[str, Any]: - """Filter playbooks by their pattern frontmatter. - - Use this when the user asks for waste patterns of a given provider, - waste category, or detection confidence - e.g. "the idle-resource - playbooks", "obvious AWS waste", "cross-cloud patterns". + """Answer "we are wasting money on X - how do I find and fix it?": + filter the runbooks by provider, service, waste category or detection + confidence. + + Use this for questions like "which VMs are running for nothing", + "why is our NAT bill so high", "what waste can we clean up safely + without review" - anything that names a provider, a waste category, or + how confident the detection needs to be before acting. All filters are optional and combine with AND semantics. String matching is case-insensitive and exact. Examples: