Skip to content

In-process use

Section 10 · From docs/MCP.md on GitHub

10.1 CloudGMCPLayer

from cloudg.mcp import CloudGMCPLayer

CloudGMCPLayer(
    config=None, *, workspace=None, policy=None, registry=None,
    include_categories=None, exclude_categories=None, include_tools=None, exclude_tools=None,
    prefix="", middleware=(), default_timeout=300.0, max_output_chars=200_000,
    name="cloudg", version=None, instructions=None,
)
Argument Default Meaning
config CloudGConfig() cloudg configuration; used only to build the default workspace
workspace Workspace(config) Shared state (section 11). Pass your own to set allowed roots or share datasets between layers
policy $CLOUDG_MCP_POLICY or standard A Policy, a profile name, a path, inline JSON or a dict
registry default_registry() Primitives to expose
include_categories / exclude_categories none Filter every kind of primitive by category
include_tools / exclude_tools none Filter tools by internal name
prefix "" Prepended to tool and prompt names; validated (at most 64 of A-Za-z0-9_.-), ValueError otherwise
middleware () Middleware list, outermost first
default_timeout 300.0 Seconds; None or 0 disables. A tool's own timeout_seconds wins
max_output_chars 200,000 Cap on the text block of one tool result
name, version, instructions "cloudg", cloudg's version, built-in text Server identity and the instructions sent on initialize / server/discover

Every keyword argument except config is also a field of LayerOptions (cloudg/mcp/layer_options.py, importable from cloudg.mcp.layer), so a configuration can be built once and passed whole: CloudGMCPLayer(config, options=LayerOptions(policy="strict", prefix="cloudg_")). Keyword arguments given next to options override its fields, and an unknown keyword raises TypeError like a normal signature would.

When any filter is given, the layer builds a new Registry with the matching specs, so the registry you pass is not modified. Without filters the layer uses your registry object as is, and later registry.add(...) calls show up in the layer.

Other attributes and methods:

Member Purpose
layer.workspace, layer.policy, layer.registry, layer.middleware The parts
layer.use(mw) Append a middleware (innermost so far); returns the layer
layer.on_change(fn) Register fn(kind, uri) for change events
layer.notify_change(kind, uri=None) Fire a change event; safe from worker threads
layer.exposed_name(name) / layer.internal_name(exposed) Add or strip the prefix
layer.context(principal=None, **fields) Build a ToolContext
layer.list_tools(p), list_resources(p), list_resource_templates(p), list_prompts(p) Specs visible to principal p
layer.tools_wire(p), resources_wire(p), resource_templates_wire(p), prompts_wire(p) The same as wire dicts
layer.get_tool(name, p) The spec, or NotFoundError
await layer.call_tool(name, arguments=None, *, principal=None, context=None) ToolResult
await layer.read_resource(uri, *, principal=None, context=None) list of TextResourceContents / BlobResourceContents
await layer.get_prompt(name, arguments=None, *, principal=None, context=None) PromptResult
await layer.complete(ref, argument, *, principal=None, context_arguments=None) {"values", "total", "hasMore"}
layer.register_into(server, **kw) Section 8
layer.serve(transport="stdio", **kw) Section 10.3

If you pass neither principal nor context, the call runs as Principal.local(). If you pass only a context, its principal is used.

10.2 Calling things

import asyncio
from pathlib import Path

from cloudg.config import CloudGConfig
from cloudg.mcp import CloudGMCPLayer, Principal, Workspace

ROOT = Path("docest")


async def main():
    ws = Workspace(CloudGConfig(), allowed_roots=[ROOT], output_dir=ROOT / "out")
    layer = CloudGMCPLayer(policy="standard", workspace=ws)

    res = await layer.call_tool("load_dataset", {"path": "inventory", "name": "prod"})
    print(res.structured["total_assets"], [c.to_wire()["type"] for c in res.content])

    res = await layer.call_tool("get_asset", {"ref": "bastion"})
    print(res.structured["max_severity"], res.meta["cloudg/duration_ms"])

    contents = await layer.read_resource("cloudg://datasets/prod/summary")
    print(contents[0].mime_type, len(contents[0].text))

    prompt = await layer.get_prompt("investigate_asset", {"ref": "bastion"})
    print([m.content.to_wire()["type"] for m in prompt.messages])

    done = await layer.complete({"type": "ref/resource", "uri": "cloudg://assets/{+ref}"},
                                {"name": "ref", "value": "web"})
    print(done)

    analyst = Principal(id="alice", roles={"analyst"})
    res = await layer.call_tool("find_assets", {"internet_exposed": True}, principal=analyst)
    print(res.is_error, res.structured["total"])


asyncio.run(main())

Results from that run (values copied from the printed output):

Call Result
load_dataset 44 assets; content is a text block plus a resource_link to cloudg://datasets/prod/summary
get_asset("bastion") max_severity HIGH; links to cloudg://assets/bastion and cloudg://assets/bastion/neighbors
read_resource(".../summary") one application/json text content
get_prompt("investigate_asset") a text message with instructions, then a resource message embedding the asset's detail
complete(... "web") {"values": ["web-1", "web-2", "web-acl", "web-alb", "web-tg", "sg-web"], "total": 6, "hasMore": false}

Completion matches substrings, which is why sg-web is offered for web. Dataset names complete too: {"type": "ref/resource", "uri": "cloudg://datasets/{dataset}/summary"} with an empty value returned {"values": ["prod"], "total": 1, "hasMore": false}, and the framework argument of the compliance_gap_analysis prompt with value CIS returned ["CIS-AWS", "CIS-Azure", "CIS-GCP"].

10.3 layer.serve

layer.serve("stdio")                                   # blocks until stdin closes
layer.serve("http", host="127.0.0.1", port=8765, auth_tokens=["env:TOKEN:analyst:ci"])

layer.serve(transport, ...) passes everything on to cloudg.mcp.server.serve(layer, transport, ...), which runs serve_async in asyncio.run and returns quietly on Ctrl+C. The async version:

from cloudg.mcp.server import serve_async

await serve_async(
    layer, transport="stdio", *, options=None, flavor="auto", host="127.0.0.1", port=8765,
    path="/mcp", auth=None, auth_tokens=None, allowed_origins=None, allowed_hosts=None,
    cors_origins=(), json_response=False, stateless=False, page_size=100,
    principal_resolver=None, log_level="INFO", ready=None,
)

The arguments mirror the CLI options. They are the fields of cloudg.mcp.server.ServeOptions, so serve_async(layer, "http", options=ServeOptions(port=9000, auth_tokens=[...])) works too; keyword arguments override the fields of options, and an unknown keyword raises TypeError. auth and auth_tokens are left out of the options' repr(), so logging the options cannot leak a token. auth takes a ready TokenAuth; auth_tokens takes spec strings. serve_async leaves the application's logging alone: it sets the cloudg logger to log_level and adds a stderr handler to cloudg only when no handler is configured anywhere. Invalid option combinations (section 5.3) raise ValueError before anything starts. When serving ends, normally or by cancellation, it calls layer.policy.save_vault() so pseudonyms issued during the session are persisted for policies with a vault path. ready is an asyncio.Future that the native HTTP server resolves with the bound (host, port) once it listens, which is how tests start a server on port 0:

import asyncio
from cloudg.mcp.server import serve_async


async def main():
    ready = asyncio.get_running_loop().create_future()
    task = asyncio.create_task(serve_async(layer, "http", flavor="native", port=0, ready=ready))
    host, port = await ready
    print(f"listening on http://{host}:{port}/mcp")
    ...                     # talk to it
    task.cancel()

To build a layer the way the CLI does, with its middleware stack, use cloudg.mcp.server.create_layer_from_options(...). It takes config, config_path, policy, datasets, prefix, the four filters, read_only, audit_log, metrics=True, cache_ttl, max_concurrency, registry, middleware (added innermost), default_timeout, workspace and any extra CloudGMCPLayer keyword. read_only_registry(registry) is the filter behind --read-only.

10.4 Principal

from cloudg.mcp import Principal

Principal(id="anonymous", roles={"default"}, attributes={})
Principal.local()        # id "local", roles {"default", "local"}

roles drive the policy; attributes are free-form and reach handlers and transforms. Principals are plain dataclasses, and the policy only reads them.

10.5 ToolContext

Every handler receives a ToolContext as its first argument (named ctx or context).

Member Meaning
ctx.layer, ctx.workspace, ctx.config The layer, its workspace, the workspace's cloudg config
ctx.principal The caller
ctx.kind, ctx.name tool, resource, prompt or completion; the tool or prompt name, the URI, or for a completion the name of the argument being completed
ctx.request_id The JSON-RPC id when an adapter supplied it
ctx.session The adapter's session object (SDK session, fastmcp context, native session) or None
ctx.meta Free-form per-call dict; completions find the other arguments in ctx.meta["arguments"]
ctx.links Resource links collected by ctx.link
await ctx.report_progress(progress, total=None, message=None) Progress notification; values that do not increase are dropped, as MCP requires
ctx.report_progress_sync(progress, total=None, message=None) The same from a sync handler in a worker thread
await ctx.log(level, data, logger_name="cloudg") MCP log notification (and the cloudg.mcp Python logger). Levels: debug, info, notice, warning, error, critical, alert, emergency; anything else becomes info
ctx.link(uri, name, *, title=None, description=None, mime_type="application/json", priority=None) Append a resource_link to the result

To receive progress and logs in-process, give the context callbacks:

async def on_progress(progress, total, message):
    print("progress", progress, total, message)

ctx = layer.context(Principal.local(), progress_callback=on_progress,
                    log_callback=lambda level, data, name: print("log", level, data, name))
res = await layer.call_tool("cloudg_progress_async", {"steps": 2}, context=ctx)

With a test tool that reports each step twice and then logs at notice, the callbacks received (1, 2, 'step 1'), (2, 2, 'step 2') (the repeated values were dropped) and ('notice', {'done': 2}, 'cloudg'). A sync tool that called ctx.report_progress_sync three times delivered (1, 3, 'step 1'), (2, 3, 'step 2') and (3, 3, 'step 3') through the same callback.