Skip to content
Draft
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
85 changes: 85 additions & 0 deletions .claude/agents/mcp-maintainer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
name: mcp-maintainer
model: sonnet
description: Keeps a repo's MCP (Model Context Protocol) surface current and correct, running as part of CI. Detects whether anything changed that requires the repo's MCP server to be built (first time) or updated (drift) — the `.fuze/manifest.json` mcp block, the server under `mcp/`, its tool manifest, and conformance to the frozen MCP contract — and if so makes the change and pushes it to the PR (or opens a separate auto-mergeable follow-up PR). Does NOT author a tool's product/domain behaviour, handle prod credentials, or deploy. Use as the automated MCP upkeep stream.
tools: Task, Bash, Glob, Grep, LS, Read, Edit, MultiEdit, Write, NotebookEdit, WebFetch, WebSearch, TodoWrite
skills: [verification-protocol, model-cascade]
---

You are the **MCP maintainer**. You keep this repo's **Model Context Protocol surface**
current — the sibling of `a2a-maintainer`, and deliberately shaped the same way. A2A is
how *agents* ask this repo for an outcome; **MCP is how an LLM session queries and
operates on this repo's objects and data directly**. You run automatically in CI on
every PR (and can be `@`-invoked). You are **upkeep, not product**: you wire the
surface, you never invent what a tool *does*.

## What "the MCP surface" is (the only things you own)

1. **`.fuze/manifest.json` `mcp` block** — `enabled`, `servers[]` (each with `name`,
`transport`, `entry`), `entryServer`. Present and internally consistent for a repo
that means to be drivable from an LLM session.
2. **The server itself** — `mcp/server.<ext>` exists for every server named in
`servers[]`, starts, and advertises a tool list.
3. **The tool manifest** — `mcp/tools.json` describing every exposed tool: `name`,
`description`, `inputSchema`, and **`mutates: true|false`**. A tool missing
`mutates` is a break (see the read/write split below).
4. **Contract currency** — the pinned MCP protocol version matches what the family
standard declares.

## First run vs drift

- **First time:** scaffold it — add the `mcp` block (default `enabled: false`), create
a minimal server skeleton that starts and lists zero tools, and `TODO`-mark every
tool the repo obviously needs but whose behaviour you must not invent.
- **Drift:** reconcile only what changed — a renamed server, a tool added to the code
but missing from `tools.json`, a schema that no longer matches the handler, a
protocol-version bump. Touch the minimum.
- **In sync:** do nothing and say so. Silence when correct is the goal; do not churn.

## The read/write split — the one judgement call you MUST NOT skip

Every tool declares `mutates`. When you scaffold or reconcile a tool, classify it, and
where a repo exposes anything sensitive, **flag rather than decide**:

- **Secret/credential material** (e.g. FuzeKeys): listing, describing, and rotating a
key are ordinary tools. A tool that returns raw secret **material** puts plaintext
into session transcripts, so it must be its own explicitly-named tool — never a
field that falls out of a `list` or `describe` response. If you find material
returned as a side effect of a read, do not silently redesign it: `NEEDS PRODUCT`
it.
- **Infrastructure** (e.g. FuzeInfra): reads are ordinary. A mutating tool must drive
the repo's own GitOps path — under Argo `selfHeal` a direct cluster patch is
reverted, so a `kubectl`-shaped write tool is broken by construction, not merely
risky. Flag it.

You classify and flag. You do not get to decide a product's exposure policy.

## Output — push to the PR, else a follow-up PR

- **On a PR (same-repo):** commit back to the PR branch so the MCP surface lands *with*
the change that affected it. Prefix `chore(mcp-maintain):` and end the commit
`[skip mcp]` so you never re-trigger yourself.
- **When you can't push** (fork PR, or a first-time build on a `push` run): open a
follow-up PR from an `mcp-maintain/**` branch, labelled **`auto-merge`**. Never
self-merge.

## Hard boundaries (flag, never fabricate)

- **Never author a tool's product/domain behaviour** — what it actually queries, which
table it reads, its real business rules. Scaffold a schema-valid skeleton and
`BLOCKED:`/`TODO` the behaviour for the owning product agent.
- **Never handle credentials or secrets**, never `kubectl`, never touch prod.
- **Never flip `enabled: true`** for a server with no working tool. An advertised
server that errors on every call is worse than one that is off.
- **Never widen a tool's `mutates: false` to `true`** to make a handler compile. If the
handler mutates, the classification was right and the *handler* is the bug.

## Done contract (report exactly this)

`MCP SURFACE: <in-sync | scaffolded | reconciled>` — then either
`SCOPE DONE (verified): <what changed + where it landed + server starts and lists tools>`
or `NO CHANGE — in sync`. Always append
`NEEDS PRODUCT/OPERATOR: <named items you TODO-flagged (tool behaviour, exposure policy, creds)>`
when the surface can't be fully live without them. Verify the server actually starts
and lists its tools before claiming done — a server that doesn't start is a bug, not a
deliverable.
50 changes: 49 additions & 1 deletion .fuze/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,26 @@
"requireSignatures": true,
"deployOnPush": false
},
"a2a": {
"enabled": false,
"servingRoles": [
"agent-orchestrator"
],
"entryRole": "agent-orchestrator",
"note": "The A2A surface here is REAL, not a scaffold: agent-templates/a2a/ implements card_generator, adapter, authz and identity, and agent-templates/roles/agent-orchestrator/role.json is a fully-described serving role. Left enabled:false only because flipping it requires verifying the card projects schema-valid against the frozen contract \u2014 a2a-maintainer's call, not a manifest edit's."
},
"mcp": {
"enabled": true,
"servers": [
{
"name": "fuzeagent",
"transport": "stdio",
"entry": "mcp-servers/fuzeagent-server/server.py"
}
],
"entryServer": "fuzeagent",
"note": "REAL, not a scaffold \u2014 mcp-servers/fuzeagent-server/ already serves 15+ tools over the hierarchy API. Outstanding: a tools.json declaring `mutates` per tool. Several tools mutate (assign_task, deploy_agent, create_custom_agent, update_task_status, create_organizational_goal, update_goal_progress) and MUST be classified mutates:true; the read tools (list_*, get_*) are mutates:false. Classification is mcp-maintainer's to reconcile."
},
"providesTo": [
"Exec-ceo",
"Exec-cfo",
Expand Down Expand Up @@ -59,5 +79,33 @@
"FuzeFront",
"FuzeKeys",
"FuzePlan"
]
],
"mobile": {
"product": "FuzeAgent",
"required": true,
"strategy": "pwa",
"targets": [
"android",
"mobile-web"
],
"responsive": {
"min_width": 375,
"breakpoints": [
375,
768,
1024
],
"min_tap_target_px": 44
},
"acceptance": [
"No horizontal scroll at min_width; the shell drawer replaces the desktop sidebar.",
"Every interactive target is at least min_tap_target_px.",
{
"check": "Lighthouse mobile performance",
"min_score": 80,
"at_width": 375
},
"The standalone URL loads with no portal chrome \u2014 it is what the APK wraps."
]
}
}
6 changes: 6 additions & 0 deletions .github/workflows/auto-merge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ on:
check_suite:
types: [completed]

# The default GITHUB_TOKEN is read-only, so `gh pr merge --auto` fails with
# "Resource not accessible by integration".
permissions:
contents: write
pull-requests: write

jobs:
auto-merge:
name: Auto Merge PR
Expand Down
104 changes: 104 additions & 0 deletions .github/workflows/mcp-maintain.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
name: MCP Maintain

# Keeps this repo's Model Context Protocol (MCP) surface current, as part of CI. The
# sibling of a2a-maintain.yml and deliberately shaped the same way: on every PR it runs
# the `mcp-maintainer` agent to check whether anything changed that requires the repo's
# MCP server to be BUILT (first time) or UPDATED (drift) — the `.fuze/manifest.json`
# mcp block, the server under `mcp/`, its tools.json, and protocol-version currency.
# If so it commits the change BACK TO THE PR BRANCH; when it can't (fork PR / first-time
# build), it opens a separate `auto-merge`-labelled follow-up PR. It never fabricates a
# tool's product behaviour, never handles creds, never deploys.
#
# WHY MCP is maintained separately from A2A: A2A is how another AGENT asks this repo for
# an outcome; MCP is how an LLM SESSION queries and operates on this repo's objects and
# data directly. Different surfaces, different contracts, different failure modes — so
# one agent per surface rather than one that half-understands both.
#
# Requires repo secret ANTHROPIC_API_KEY. No key -> SKIPS (never red).

on:
pull_request:

permissions:
contents: write # commit the reconciled MCP surface back to the PR branch
pull-requests: write # open a follow-up PR when a push-back isn't possible
id-token: write # claude-code-action fetches an OIDC token (required)
actions: read

concurrency:
group: mcp-maintain-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
mcp-maintain:
# Loop guard: never run on our own maintenance branches.
if: ${{ !startsWith(github.event.pull_request.head.ref, 'mcp-maintain/') }}
runs-on: ubuntu-latest
steps:
- name: Skip if key absent
id: guard
env:
KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
if [ -z "$KEY" ]; then
echo "::notice::ANTHROPIC_API_KEY not set — MCP maintain skipped (set it to enable)."
echo "ok=false" >> "$GITHUB_OUTPUT"
else
echo "ok=true" >> "$GITHUB_OUTPUT"
fi

- name: Checkout PR head (writable, same-repo only)
if: steps.guard.outputs.ok == 'true'
uses: actions/checkout@v4

Check warning

Code scanning / Semgrep OSS

Semgrep Finding: yaml.github-actions.security.github-actions-mutable-action-tag.github-actions-mutable-action-tag Warning

GitHub Actions step uses a mutable tag or branch reference. Tags and branch names can be silently repointed by the action owner, enabling supply-chain attacks — as seen in the trivy-action and kics-github-action compromises. Pin the reference to a full 40-character commit SHA instead, e.g. uses: actions/checkout@8ade135a41bc03ea155e62e844d188df1ea18608.
with:
ref: ${{ github.event.pull_request.head.ref }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
fetch-depth: 0

- name: Skip on last-commit marker
id: marker
if: steps.guard.outputs.ok == 'true'
run: |
if git log -1 --pretty=%B | grep -q '\[skip mcp\]'; then
echo "::notice::last commit marked [skip mcp] — skipping to avoid a self-trigger loop."
echo "run=false" >> "$GITHUB_OUTPUT"
else
echo "run=true" >> "$GITHUB_OUTPUT"
fi

- name: Run mcp-maintainer
if: steps.guard.outputs.ok == 'true' && steps.marker.outputs.run == 'true'
uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
prompt: |
You are the **mcp-maintainer** for this repo (follow `.claude/agents/mcp-maintainer.md`
exactly — same scope, boundaries, and done-contract). This is an automated CI run on
PR #${{ github.event.pull_request.number }} (head `${{ github.event.pull_request.head.ref }}`).

TASK: assess whether this PR changed anything that requires the repo's MCP surface to be
built (first time) or updated (drift) — the `.fuze/manifest.json` `mcp` block, the
server(s) under `mcp/` named in `servers[]`, the `mcp/tools.json` tool manifest
(every tool needs name/description/inputSchema/`mutates`), and protocol-version
currency. If the surface is already in sync, DO NOTHING and say so.

IF a change is needed:
- Make the minimal correct change (scaffold on first run; reconcile only what drifted).
- COMMIT IT BACK TO THIS PR BRANCH: `chore(mcp-maintain): <what> [skip mcp]` and push to
`${{ github.event.pull_request.head.ref }}`. The trailing `[skip mcp]` is REQUIRED so you
do not re-trigger yourself.
- If you cannot push to the PR branch (this is a fork PR), instead open a follow-up PR from
an `mcp-maintain/pr-${{ github.event.pull_request.number }}` branch, add the `auto-merge`
label, and comment the link on this PR.

HARD BOUNDARIES: never fabricate a tool's product/domain behaviour (scaffold a schema-valid
skeleton and TODO-flag the real behaviour), never handle secrets/creds, never `kubectl`/deploy,
never flip `enabled: true` for a server with no working tool, and never widen a tool's
`mutates: false` to `true` just to make a handler compile — if the handler mutates, the
handler is the bug. Where the repo exposes secret material or infrastructure writes, FLAG the
exposure policy rather than deciding it. Verify any server you touch actually starts and
lists its tools.

End with the done-contract line from your agent def
(`MCP SURFACE: <in-sync|scaffolded|reconciled>` + what landed + `NEEDS PRODUCT/OPERATOR:` if any).
claude_args: '--allowedTools "Bash,Read,Edit,MultiEdit,Write,Glob,Grep,WebFetch"'
82 changes: 82 additions & 0 deletions registration/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# FuzeFront registration

FuzeAgent self-registers with the FuzeFront portal at deploy time.

| File | Purpose |
|---|---|
| `manifest.json` | App identity, Module-Federation contract, `nav` placement |
| `policy.json` | FuzeAgent's own Permit resources/roles, bare keys |
| `register.sh` | Idempotent registration script from `@fuzefront/onboarding-kit` |

## This replaces a hardcoded entry in FuzeFront

FuzeAgent is currently one of only three apps in the portal, and it is registered by a
**hardcoded entry in FuzeFront's `backend/applications/src/app-registry/builtins.ts`** —
i.e. FuzeFront's source has to change for FuzeAgent's portal presence to change. Once
this self-registration is live, that builtin can be retired (deliberately last, and
behind a flag, so the app never vanishes from the menu in the gap).

## Module Federation is real here

Unlike most siblings, `integration.type` is genuinely `module-federation`. The
contract in `manifest.json` is copied from `services/ui-react/vite.config.ts`:

| Field | Value | Source |
|---|---|---|
| `scope` | `fuzeagentApp` | federation `name` |
| `module` | `./FuzeAgentApp` | `exposes` key |
| `remoteEntry` | `https://fuzeagent.prod.fuzefront.com/remoteEntry.js` | `filename` + host |

React and react-dom are shared singletons, so FuzeFront's React instance is reused —
which is what makes mounting into the host shell work at all.

## Menu placement

```jsonc
"nav": { "section": "build", "order": 10 }
```

FuzeAgent leads the **build** stage. Menu order was previously unexpressible: the
registry sorted by `created_at`, so the side menu was in registration order.

## Policy

Derived from `services/orchestrator/models.py`: `Organization`, `Team`, `Agent`,
`Task`, plus `Goal` (the organizational-goal surface the MCP server exposes).

`Agent:deploy` and `Task:assign` are separated from ordinary writes deliberately —
they cause an autonomous agent to actually *run*, which is a different kind of
authority from editing a record.

## MCP is already real

`.fuze/manifest.json` declares `mcp.enabled: true` — not a scaffold.
`mcp-servers/fuzeagent-server/` already serves 15+ tools against the hierarchy API.

**Outstanding:** a `tools.json` declaring `mutates` per tool. Several tools mutate and
must be classified `mutates: true`:

- `assign_task`, `deploy_agent`, `create_custom_agent`, `update_task_status`,
`create_organizational_goal`, `update_goal_progress`, `create_goal_conversation`

The `list_*` / `get_*` tools are `mutates: false`. `deploy_agent` deserves particular
care: it starts an autonomous agent, so it must never be reachable as a side effect of
a read. Classification is `mcp-maintainer`'s to reconcile.

## A2A is also real

`agent-templates/a2a/` implements card generation, the adapter, authz and identity, and
`agent-templates/roles/agent-orchestrator/role.json` is a fully-described serving role.
The manifest names it accurately but leaves `enabled: false`: flipping it requires
verifying the card projects schema-valid against the frozen contract, which is
`a2a-maintainer`'s call rather than a manifest edit's.

## NOT DONE — init container not wired

`deploy/helm/fuzeagent/` exists, but this repo is a multi-service deployment and
exactly one deployment must run registration; wiring more than one would have them race
and duplicate-register. Choosing the owner is `devops-engineer`'s call and is flagged
rather than guessed. To finish: paste the init container from
[`@fuzefront/onboarding-kit`](https://github.com/izzywdev/FuzeFront/blob/master/packages/onboarding-kit/helm/initcontainer.yaml)
into that one pod spec, and create the `fuzefront-registration` Secret plus a ConfigMap
of this directory.
22 changes: 22 additions & 0 deletions registration/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"manifestVersion": "1",
"slug": "fuzeagent",
"name": "FuzeAgent",
"menuLabel": "Agents",
"description": "AI team orchestration — autonomous agents (Claude Code SDK + CrewAI) organised into teams and coordinated by a digital CEO.",
"icon": { "kind": "emoji", "value": "🤖" },
"mode": "portal",
"modes": ["portal", "standalone"],
"builtin": false,
"integration": {
"type": "module-federation",
"remoteEntry": "https://fuzeagent.prod.fuzefront.com/remoteEntry.js",
"scope": "fuzeagentApp",
"module": "./FuzeAgentApp"
},
"nav": { "section": "build", "order": 10 },
"chrome": { "menu": "host", "topbar": "host" },
"routing": { "path": "/app/fuzeagent", "host": "fuzeagent.fuzefront.com" },
"visibility": "organization",
"roles": []
}
Loading
Loading