Repository navigation
Description
Summary
detect declines a workspace outright. init — the command the control loop routes to from verify --preview — writes a manifest for it anyway, and the manifest it writes declares a source type it did not infer and did not flag for review.
A user following the documented loop on a Python MCP server ends up committing an artifact that detect had just said was pointless, describing an OpenAPI spec that does not exist in the repository.
Reproduction
awslabs/mcp#4489 — a FastMCP Python MCP server under src/billing-cost-management-mcp-server. CLI 0.16.0b7 at fc96bad9.
Step 1 — preview routes to init. Scope resolution works correctly here, picking the right sub-project out of a monorepo with dozens of servers:
control.next_action.command:
shipgate init --workspace …/src/billing-cost-management-mcp-server --write --json
why: "Every changed path that carries a capability surface belongs to
src/billing-cost-management-mcp-server…"
Step 2 — detect on that exact path declines it:
is_agent_project : false
frameworks : []
suggested_sources: []
next_action : "Stop: Python project with no agent framework, prompts, or tool
surface — not a Shipgate target."
Step 3 — init on that exact path writes a manifest:
created: true manifest_status: written agent_name: None
placeholders: ['agent.name', 'agent.declared_purpose[0]',
'tool_sources[0].id', 'tool_sources[0].path']
tool_sources:
- id: CHANGE_ME
type: openapi
path: CHANGE_ME.yamlThree distinct defects
1. Two commands answer one question differently. detect says "not a Shipgate target"; init proceeds. Because verify --preview routes to init and not to detect, a user following control.allowed_next_commands never sees the refusal. The product leads them to create the artifact its own discovery command says has no purpose.
2. type is a silent guess where id and path are flagged. Both CHANGE_ME fields appear in placeholders[]; type: openapi appears in neither, so nothing tells the reader it needs review. Filling in the two flagged blanks yields a schema-valid manifest declaring an OpenAPI source for a FastMCP Python server — the exact outcome quickstart warns about: "the manifest validates but downstream consumers see meaningless defaults."
The fallback is deliberate and its reasoning is sound — cli/discovery/template.py:94-102:
# Schema requires ≥ 1 of tool_sources / openai_api / anthropic /
# google_adk / langchain / crewai. Empty workspace → emit a
# CHANGE_ME stub identical to the legacy --minimal template's
# fallback so users get an actionable starting point.Satisfying the schema is the right instinct. Picking one of six possible types and not marking it as a guess is the defect: the stub is "actionable" only if the reader knows to question a field the tool did not question.
3. has_tools_dir: false with a tools/ directory present. The server's tools live at awslabs/billing_cost_management_mcp_server/tools/; the conventional-dir check looks only at the workspace root, so the one structural signal this repository does offer is missed. Contributing cause to defect 1's verdict, minor on its own.
Why this class matters more than one repository
Four vendor MCP servers walked, three languages:
| repo | language | registration | detect sees it? |
|---|---|---|---|
| mongodb-mcp-server | TypeScript | static toolName = "aggregate" |
no |
| mcp-grafana | Go | mcpgrafana.MustTool("update_incident", …) |
no |
| awslabs/mcp | Python | @server.tool(name='budgets', …) |
no |
| github-mcp-server | Go | generates .snap JSON |
yes |
Three of four land in exactly this path — detect declines, init writes a stub anyway. That is the default experience for an MCP server today (#431), which makes this the shape most first-time adopters of that class will hit.
Expected
initon a workspacedetectdeclines either refuses with the same reason, or writes and states plainly that discovery found no tool surface and the source block is a scaffold rather than an inference.- Any field the tool chose without evidence is marked as needing review — either
type: CHANGE_MEwith the accepted values listed (as the declaration questionnaire already does foreffect), or a comment saying which types are available and that this one was not inferred. detectandinitreach the same conclusion about whether a workspace is a target, or the disagreement is stated where the reader is.
Acceptance
- The reproduction above produces either a refusal or a manifest whose
typeis visibly unresolved. - A workspace
detectaccepts is unaffected (regression: the ADK and MCP samples). -
placeholders[]includes every field the template chose without evidence. - A nested
tools/directory contributes its conventional-dir signal.
Found 2026-08-24 walking awslabs/mcp#4489. Related: #431 (why three of four MCP servers reach this path), #328 (product defect vs. user configuration error in adopter-facing output), and cause 2 of #440 (two surfaces answering one question with no precedence).
Activity
fix(detect): name the conventional directory a reader can open (#441 …
perf(detect): walk directories, not files, to locate conventional dirs (
docs(changelog): record the conventional-dir path, root predicate, an…
refactor(scaffold): drop an unused property and a schema rule stated …
fix(init): address the seven self-review findings on #441
fix(scaffold): restore the whole string contract, and stop denying ev…
fix(init): a field the tool chose without evidence is marked as one (#…
Read the Python `@mcp.tool` decorator, in both detectors (#484) (#535)