Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 43 additions & 14 deletions .claude/skills/coact-publish/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ description: >-
functions for Claude", "turn this into a Claude extension/connector", "publish
a local MCP server", "wrap my tools as a Claude Desktop extension". Also use to
draft an integration from a natural-language description ("describe an
integration", "I want a Claude connector that can…") via `coact describe`. For
REMOTE claude.ai connectors (HTTPS + OAuth) this is the wrong target — that
surface is not built yet (see Limitations).
integration", "I want a Claude connector that can…") via `coact describe`. Also
covers REMOTE claude.ai connectors (a hosted Streamable-HTTP MCP server + OAuth
2.1) via the `claude-remote-connector` target — use when the user wants a
cloud-reachable connector, not a local install.
metadata:
version: 0.2.0
version: 0.3.0
---

# coact publish — Python capability → Claude integration
Expand Down Expand Up @@ -91,19 +92,47 @@ Install the result: Claude Desktop → Settings → Extensions → Install Exten
(or double-click the `.mcpb`). The extension runs **on the user's machine** and
needs a Python with `py2mcp` + `fastmcp` importable.

## Remote claude.ai connector (Streamable-HTTP + OAuth 2.1)

A claude.ai **custom connector** is a *remote* MCP server reached from Anthropic's
cloud over HTTPS + OAuth — a different surface from the local `.mcpb`. Scaffold one
(a hosted service you deploy) with the `claude-remote-connector` target:

```bash
coact publish mypkg.tools:summarize --target claude-remote-connector \
--name my-conn --dest ./out \
--connector-url https://my-conn.example.com --idp-issuer https://my-idp.example.com
# → ./out/my-conn-connector/ (server/app.py, connector_config.json, requirements.txt,
# DEPLOY.md, Dockerfile)
```

```python
from coact import publish_remote
publish_remote(["mypkg.tools:summarize"], name="my-conn", dest="out",
connector_url="https://my-conn.example.com",
idp_issuer="https://my-idp.example.com",
required_scopes=["mcp:read"])
```

It scaffolds an OAuth 2.1 **resource server** (validates a managed IdP's JWTs via
`py2mcp.http.mk_http_app`; never issues tokens; audience-bound per RFC 8707). Omit
`--connector-url`/`--idp-issuer` to scaffold with **fill-in placeholders + a loud
warning**. Then follow the generated `DEPLOY.md`: set your IdP, run behind TLS
(`uvicorn server.app:app`), and add the HTTPS URL as a custom connector in claude.ai.
Needs `py2mcp>=0.1.4` + `fastmcp` + `uvicorn` where the service runs.

## Key distinctions (don't conflate)

- **Local `.mcpb` (this target):** stdio, no OAuth, runs on the user's machine.
- **Remote claude.ai connector (NOT this target):** a remote MCP server reached
from Anthropic's cloud over HTTPS + OAuth — a different surface, not yet built.
- A `.mcpb` is *connectivity* (tools). A **Skill** (`SKILL.md`) is *procedural
knowledge*. They are complementary; this skill packages the former.
- **Local `.mcpb` (`claude-local-mcpb`):** stdio, no OAuth, runs on the user's machine.
- **Remote connector (`claude-remote-connector`):** a hosted MCP server reached from
Anthropic's cloud over HTTPS + OAuth — public, multi-user, you deploy it.
- A connector is *connectivity* (tools). A **Skill** (`SKILL.md`) is *procedural
knowledge*. They are complementary.

## Limitations (current)

- Only `claude-local-mcpb`. Remote connectors, Claude Code plugins, ChatGPT
Apps, and Gemini are planned targets (the registry is open-closed).
- The bundle references tools by `module:function`; the **functions must be
importable** in the Python that Claude Desktop runs (full dependency vendoring
into the bundle is a future refinement).
- Targets: `claude-local-mcpb` and `claude-remote-connector`. Claude Code plugins,
ChatGPT Apps, and Gemini are planned (the registry is open-closed).
- Tools are referenced by `module:function`; the **functions must be importable**
where the server runs (dependency vendoring is a future refinement).
- Background: `misc/docs/CHATBOT_INTEGRATION_LANDSCAPE.md`.
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,27 @@ publish(["mypkg.tools:summarize", "mypkg.tools:translate"],

Sources can be `module:function` refs, live callables, or a skill carrying a
`coact: mcp:` block. `dry_run=True` (or `--dry-run`) previews the bundle without
writing it. This is the **local** surface (stdio, no OAuth); remote claude.ai
*connectors* (HTTPS + OAuth), Claude Code plugins, ChatGPT Apps, and Gemini are
planned targets on the same open-closed registry. Background:
writing it. This is the **local** surface (stdio, no OAuth). Install: `pip install coact[mcpb]`.

For the **remote** surface — a claude.ai *custom connector* (a hosted
Streamable-HTTP MCP server reached from Anthropic's cloud over HTTPS + OAuth 2.1) —
use the `claude-remote-connector` target, which scaffolds a deployable service:

```python
from coact import publish_remote
publish_remote(["mypkg.tools:summarize"], name="my-conn", dest="out",
connector_url="https://my-conn.example.com", # this server's public URL
idp_issuer="https://my-idp.example.com") # your managed IdP
# → out/my-conn-connector/ (server/app.py + connector_config.json + DEPLOY.md + …)
```

The scaffold is an OAuth 2.1 **resource server** (validates a managed IdP's
audience-bound JWTs; never issues tokens) built by
[`py2mcp`](https://github.com/i2mint/py2mcp)'s `http.mk_http_app`; coact writes the
deploy packaging, py2mcp/FastMCP serves the MCP. Follow the generated `DEPLOY.md`.
Claude Code plugins, ChatGPT Apps, and Gemini are further planned targets on the
same open-closed registry. Background:
[`misc/docs/CHATBOT_INTEGRATION_LANDSCAPE.md`](misc/docs/CHATBOT_INTEGRATION_LANDSCAPE.md).
Install: `pip install coact[mcpb]`.

There are two ways to get an `IntegrationSpec`. The **mechanical** ingress above
(refs / callables / skills) uses **no LLM**. The **opt-in** ingress refines a
Expand Down
2 changes: 2 additions & 0 deletions coact/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
from coact.nl_ingress import integration_spec_from_description
from coact.publish import PublishResult, publish, publish_targets
from coact.publish_mcpb import publish_mcpb # registers 'claude-local-mcpb'
from coact.publish_remote import publish_remote # registers 'claude-remote-connector'
from coact.scaffold import scaffold_fleet
from coact.stores import AgentStore, agents_dir
from coact.synthesis import synthesize_persona, synthesize_return_contract
Expand Down Expand Up @@ -133,6 +134,7 @@ def _resolve_version() -> str:
"publish_targets",
"PublishResult",
"publish_mcpb",
"publish_remote", # remote claude.ai connector (Streamable-HTTP + OAuth 2.1; D19)
# Scaffold (the one topology-adjacent emitter — a starter you own; D8)
"scaffold_fleet",
# Synthesis & LLM facade
Expand Down
21 changes: 18 additions & 3 deletions coact/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,15 +147,30 @@ def publish(
name: str | None = None,
author: str | None = None,
dry_run: bool = False,
connector_url: str | None = None,
idp_issuer: str | None = None,
) -> str:
"""Publish a capability to a chatbot host (default: a local Claude Desktop .mcpb bundle).

``--dry-run`` previews the bundle members (manifest + server) without writing
the ``.mcpb``.
``--target claude-remote-connector`` instead scaffolds a REMOTE connector
(Streamable-HTTP MCP server + OAuth 2.1); ``--connector-url`` (its public URL)
and ``--idp-issuer`` (your managed identity provider) configure OAuth — omit
them to scaffold with fill-in placeholders. ``--dry-run`` previews without writing.
"""
src = source if len(source) > 1 else source[0]
extra: dict = {}
if connector_url is not None or idp_issuer is not None:
if target != "claude-remote-connector":
raise SystemExit(
"--connector-url/--idp-issuer apply only to "
f"--target claude-remote-connector, not {target!r}."
)
if connector_url is not None:
extra["connector_url"] = connector_url
if idp_issuer is not None:
extra["idp_issuer"] = idp_issuer
res = _publish(
src, target=target, dest=dest, name=name, author=author, dry_run=dry_run
src, target=target, dest=dest, name=name, author=author, dry_run=dry_run, **extra
)
return res.render()

Expand Down
Loading
Loading