@mcp-com-ai/hapi 1.1.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ ---
2
+ name: hapi-intent-based-apis
3
+ description: Make an OpenAPI 3.x or Arazzo 1.1 contract AI-ready and intent-based so HAPI's runtime serves it as MCP tools that agents discover, call safely, and plan with — no code, only a better contract. Adds discoverable operations, MCP tool hints, x-hapi-intent/effects/capability/prerequisites, x-hapi-planning facts, x-hapi-client-facts, x-hapi-outcome, x-hapi-event, and planning-eligible Arazzo workflows, then verifies with hapi graph compile/audit/simulate and an MCP smoke test. Use whenever a user wants to make an API "intent-based", "agent-ready", or "AI-friendly", add capability graph or HCG metadata, fix tools the agent never finds or calls at the wrong time, reduce MCP context bloat with progressive discovery, or fix capability_plan results that always say clarification-required — even if they only say "enrich my spec", "annotate my OpenAPI for HAPI", or "why can't the agent book/cancel/update".
4
+ license: MIT
5
+ compatibility: Requires the HAPI CLI with the Capability Graph plugin (`hapi graph --help`) and Bun for the bundled checker. Arazzo checks need the Arazzo plugin (`hapi arazzo --help`).
6
+ metadata:
7
+ author: mcp.com.ai agent-skills
8
+ version: "1.0.0"
9
+ allowed-tools: Bash(hapi:*) Bash(bun:*) Bash(curl:*) Read Write Edit
10
+ ---
11
+
12
+ # Enrich an API contract for intent-based agents
13
+
14
+ An intent-based API tells an agent three things the raw OpenAPI document does
15
+ not: **when** an operation is the right choice, **what it changes**, and **what
16
+ must already be true** before it runs. HAPI turns that metadata into focused
17
+ tool search, MCP tool hints, and a deterministic HAPI Capability Graph (HCG)
18
+ that `capability_context` and `capability_plan` expose to any MCP client, such
19
+ as OrcA in VS Code or an AI teammate like Clooney from Clawne Me.
20
+
21
+ HAPI is code-less: if the API follows good practices, the contract is the
22
+ whole integration. HAPI reads it at startup, serves every operation as an MCP
23
+ tool, compiles the graph, and answers context and planning requests from it;
24
+ OrcA and other MCP clients act on what it says. So every field is written for
25
+ its **runtime effect** (`references/runtime-behavior.md`), and every statement
26
+ must be **true for the API's behavior** as the contract, its documentation, and
27
+ the domain owner describe it. Metadata that misstates behavior is worse than
28
+ none: the planner trusts it.
29
+
30
+ ## Good API practices the contract must show
31
+
32
+ - One operation per business action, with a stable `operationId`.
33
+ - Searches/lists separate from selections and mutations; identifiers returned
34
+ by one operation and accepted by the next.
35
+ - Meaningful status codes for business failures (409, 410, 422) with an error
36
+ body that names the next step.
37
+ - `security` declared; servers declared (or `--url` for headless).
38
+ - Descriptions written for the reader who must decide what to do next.
39
+
40
+ ## Inputs to collect
41
+
42
+ - The OpenAPI 3.x document (and Arazzo 1.1 document, if any). Never overwrite
43
+ the original without permission; edit a copy or work on a branch.
44
+ - What the API does, from the contract itself (paths, schemas, responses,
45
+ security, existing descriptions), its public documentation, and the domain
46
+ owner. Ask the owner only what the contract cannot say (which choices a
47
+ person makes, which actions need confirmation, which errors mean "pick
48
+ again"). Mark anything you infer as an assumption in the report.
49
+ - The users' language and the business narrative: who the actors are, which
50
+ choices people make, which actions need confirmation.
51
+
52
+ If the document is invalid, stop and report the validator output first.
53
+
54
+ ## Workflow
55
+
56
+ ### 1. Inventory operations and the business journey
57
+
58
+ Run the checker to get a baseline, then read each operation:
59
+
60
+ ```bash
61
+ bun <skill-dir>/scripts/check-intent-metadata.ts ./openapi.yaml
62
+ ```
63
+
64
+ For each operation record what it reads or changes, which inputs must come
65
+ from an earlier result or a person, which declared errors force a new
66
+ decision, whether repeating it is safe, who may call it (`security`), and
67
+ whether it notifies anyone (webhooks, callbacks). Where the contract is silent,
68
+ use the API documentation or ask the owner; never guess effects.
69
+
70
+ Build the domain table before writing YAML:
71
+
72
+ | Operation | Kind (search / select / mutate) | Requires | Produces | Removes | Failure outcomes |
73
+
74
+ An empty selection step in front of a mutation is a gap: either the API has a
75
+ selection operation or a person makes the choice (a client fact).
76
+
77
+ ### 2. Make every operation discoverable
78
+
79
+ HAPI search matches tool names, descriptions, and arguments, word by word.
80
+ OrcA lists each deferred tool by name with the first sentence of its
81
+ description (about 80 characters) so the model knows the tool exists.
82
+
83
+ - Stable, verb-first, unique `operationId` (it becomes the tool name).
84
+ - `summary` starts with the action and the business object, in the users'
85
+ language. Add the users' own terms when the API language differs.
86
+ - First description sentence: what it does, for whom. Following sentences:
87
+ where inputs come from ("Use the `slotId` returned by getAvailability"),
88
+ limits, and what not to do.
89
+ - Every parameter and body property says the source of its value.
90
+ - Never rename an `operationId` that clients already use without telling the user.
91
+
92
+ ### 3. Set MCP tool hints that match behavior
93
+
94
+ HAPI derives MCP annotations only from `x-readOnlyHint`, `x-destructiveHint`,
95
+ `x-idempotentHint`, and `x-openWorldHint`; otherwise `GET` is read-only and
96
+ `POST`/`PUT`/`PATCH`/`DELETE` are destructive. Clients such as OrcA ask for
97
+ confirmation before any tool that is not read-only, so set hints explicitly
98
+ when the method misleads (search via `POST`, non-destructive create, idempotent
99
+ `PUT`). Base each hint on what the operation does, not on its HTTP method.
100
+
101
+ ### 4. Add discovery semantics
102
+
103
+ Add `x-hapi-capability`, `x-hapi-intent`, `x-hapi-effects`, and
104
+ `x-hapi-prerequisites` (exact shapes and validation in
105
+ `references/extension-rules.md`):
106
+
107
+ - `x-hapi-capability`: one or more taxonomy paths (`Domain/Area/Action`).
108
+ - `x-hapi-intent.use-when` / `avoid-when`: concrete situations that separate
109
+ similar operations (book vs reschedule, search vs list mine).
110
+ - `x-hapi-effects.mode` ∈ `read | create | update | delete | action`, plus
111
+ `side-effecting`, `destructive`, `idempotent`, `requires-confirmation` as the
112
+ API documents them. `requires-confirmation: true` adds the `mutation-confirmation` gate.
113
+ - `x-hapi-prerequisites`: human-readable readiness. It never creates machine state.
114
+
115
+ ### 5. Model business state with `x-hapi-planning`
116
+
117
+ - Facts are lowercase canonical predicates (`^[a-z][a-z0-9]*([._-][a-z0-9]+)*$`)
118
+ describing **verified business state**: `slot.selected`, `appointment.booked`.
119
+ Never commands (`bookAppointment`), wishes (`user.wants.x`), or guesses.
120
+ - A search produces `<things>.consulted`; it **never** produces `<thing>.selected`.
121
+ - `produces` is required and non-empty; `cost` defaults to 1 and must be finite
122
+ and ≥ 0; no duplicates; a fact cannot be both produced and removed.
123
+ - Cancellations/deletions `remove` the state they end.
124
+ - Cross-domain `requires` only for real business rules the owner confirms.
125
+ - Never invent transitions from prose or HTTP verbs; HAPI never does either.
126
+
127
+ ### 6. Declare facts only people can establish
128
+
129
+ At the document root, add `x-hapi-client-facts` for selections, confirmations,
130
+ and identity checks that happen in the conversation. Each `description` says
131
+ who establishes it and how (for example, identity from the chat channel).
132
+ Declaring a fact never makes it true; the client still verifies it.
133
+
134
+ ### 7. Declare failure outcomes and events
135
+
136
+ - For each declared non-2xx response that forces a new decision (409
137
+ unavailable, 410 expired, 422 rule violation; not 400, 401/403, or 5xx),
138
+ add `x-hapi-outcome` with `produces`/`removes`. Status key: a status code,
139
+ `NXX`, or `default`, never `2xx`. The operation must have `x-hapi-planning`.
140
+ Update the response `description` to name the next question to ask.
141
+ - For OpenAPI 3.1 `webhooks`/`callbacks` the API documents, add
142
+ `x-hapi-event.produces`. Never on ordinary `paths` operations.
143
+
144
+ ### 8. Arazzo workflows (when present)
145
+
146
+ Give a workflow `x-hapi-planning` only when it is one atomic business
147
+ transition: resolved OpenAPI steps only, no nested workflows, no dynamic
148
+ success/failure branching. Derive its facts from its steps: `requires` = step
149
+ requirements no earlier step produces; `produces` = every step's produced fact
150
+ (plus an aggregate if useful); `removes` = every removed fact. Validate with
151
+ `hapi arazzo validate --strict`. For several APIs, use the `hapi-federated-apis` skill.
152
+
153
+ ### 9. Verify, then iterate
154
+
155
+ ```bash
156
+ bun <skill-dir>/scripts/check-intent-metadata.ts ./openapi.yaml
157
+ hapi graph compile ./openapi.yaml --out graph.json
158
+ hapi graph audit graph.json
159
+ hapi graph simulate graph.json --initial-fact <client facts…> --goal-fact <goal> \
160
+ --satisfied-gate mutation-confirmation --eligible-operation <operationId>
161
+ hapi graph visualize graph.json --goal-fact <goal> --out flow.html --initial-view flow-cards
162
+ ```
163
+
164
+ - `compile` fails with pointer-scoped errors on malformed metadata; fix them all.
165
+ - `audit` must report no orphan requirements and every intended goal `planned`
166
+ (exit 0). Document intentional gaps.
167
+ - `simulate` each important goal three ways: nothing verified
168
+ (`clarification-required`), all facts and gates (`planned`), already true (`complete`).
169
+ - Optionally smoke-test the MCP surface:
170
+ `hapi serve --specs ./openapi.yaml --capability-graph --capability-planning --dev`
171
+ then call `tool_search` and `capability_context` with real user phrasing in
172
+ the users' language (see `references/examples.md`).
173
+
174
+ ### 10. Report
175
+
176
+ Return: the changed file path; a per-operation table of what you added and why
177
+ (its runtime effect); the fact vocabulary (client facts, outcomes, goals); audit and
178
+ simulation results; assumptions that need the owner's confirmation; and any
179
+ `operationId` or response change that affects existing clients.
180
+
181
+ ## Guardrails
182
+
183
+ - Do not fabricate security, business policy, operations, or Links to make a
184
+ graph connect. Report the gap instead.
185
+ - Never add a Link that maps a search result into a selection input; selection
186
+ is a person's decision.
187
+ - Keep metadata in the source contract, not in a generated derivative; if the
188
+ project generates JSON from YAML, regenerate it.
189
+ - No secrets, real customer data, or production identifiers in examples.
190
+ - Do not ask for or rely on implementation code; improve the contract. When the
191
+ API itself breaks good practice (no stable IDs, no selection operation,
192
+ errors without status codes), report it as an API design gap.
193
+
194
+ ## Files
195
+
196
+ - `references/extension-rules.md` — exact shapes, defaults, and compiler diagnostics.
197
+ - `references/runtime-behavior.md` — what HAPI's runtime and OrcA do with each contract element.
198
+ - `references/examples.md` — a full before/after operation, client facts, outcome, Arazzo workflow, and smoke-test calls.
199
+ - `scripts/check-intent-metadata.ts` — static checker (exit 1 on errors).
@@ -0,0 +1,135 @@
1
+ # Examples (clinic appointments)
2
+
3
+ ## Before
4
+
5
+ ```yaml
6
+ paths:
7
+ /appointments:
8
+ post:
9
+ operationId: createAppt
10
+ summary: Create
11
+ requestBody: { $ref: "#/components/requestBodies/Appt" }
12
+ responses:
13
+ "201": { description: Created }
14
+ "409": { description: Conflict }
15
+ ```
16
+
17
+ ## After
18
+
19
+ ```yaml
20
+ x-hapi-client-facts:
21
+ - predicate: patient.identity.verified
22
+ description: The phone number comes from the chat channel, never from typed text.
23
+ - predicate: professional.selected
24
+ description: The patient picked one professional from searchProfessionals results.
25
+ - predicate: slot.selected
26
+ description: The patient picked one free slot from getAvailability results.
27
+ - predicate: appointment.details.confirmed
28
+ description: The patient confirmed the appointment summary.
29
+
30
+ paths:
31
+ /appointments:
32
+ post:
33
+ operationId: bookAppointment
34
+ summary: Book an appointment for the patient
35
+ description: |
36
+ Books a confirmed appointment in the selected professional's free slot.
37
+ Use the `slotId` returned by getAvailability; a constructed value returns `400`.
38
+ x-destructiveHint: false
39
+ x-idempotentHint: false
40
+ x-hapi-capability: [Healthcare/Appointments/Booking]
41
+ x-hapi-intent:
42
+ use-when:
43
+ - The patient selected a professional and a free slot and confirmed the details.
44
+ avoid-when:
45
+ - The patient only wants to see free slots or existing appointments.
46
+ - The patient wants to move an existing appointment (use rescheduleAppointment).
47
+ x-hapi-effects:
48
+ mode: create
49
+ side-effecting: true
50
+ destructive: false
51
+ idempotent: false
52
+ requires-confirmation: true
53
+ x-hapi-prerequisites:
54
+ - The patient identity, the professional, and the slot are verified.
55
+ x-hapi-planning:
56
+ requires:
57
+ - predicate: patient.identity.verified
58
+ - predicate: professional.selected
59
+ - predicate: slot.selected
60
+ - predicate: appointment.details.confirmed
61
+ produces:
62
+ - predicate: appointment.booked
63
+ cost: 1
64
+ responses:
65
+ "201":
66
+ description: Appointment booked; relay `mensaje` to the patient.
67
+ "409":
68
+ description: The slot was taken; ask the patient to pick another slot.
69
+ x-hapi-outcome:
70
+ produces: [{ predicate: slot.unavailable }]
71
+ removes: [{ predicate: slot.selected }]
72
+ ```
73
+
74
+ ## Search vs selection
75
+
76
+ ```yaml
77
+ /professionals/search:
78
+ post:
79
+ operationId: searchProfessionals
80
+ summary: Search verified professionals by specialty or name
81
+ x-readOnlyHint: true
82
+ x-destructiveHint: false
83
+ x-idempotentHint: true
84
+ x-hapi-effects: { mode: read }
85
+ x-hapi-planning:
86
+ produces: [{ predicate: professionals.consulted }]
87
+ ```
88
+
89
+ ## Cancellation removes state
90
+
91
+ ```yaml
92
+ x-hapi-planning:
93
+ requires:
94
+ - predicate: patient.identity.verified
95
+ - predicate: appointment.selected
96
+ - predicate: cancellation.confirmed
97
+ produces: [{ predicate: appointment.cancelled }]
98
+ removes: [{ predicate: appointment.booked }]
99
+ ```
100
+
101
+ ## Planning-eligible Arazzo workflow
102
+
103
+ ```yaml
104
+ workflows:
105
+ - workflowId: bookAndNotify
106
+ x-hapi-planning:
107
+ requires:
108
+ - predicate: patient.identity.verified
109
+ - predicate: professional.selected
110
+ - predicate: slot.selected
111
+ - predicate: appointment.details.confirmed
112
+ produces:
113
+ - predicate: appointment.booked
114
+ - predicate: patient.notified
115
+ steps:
116
+ - stepId: book
117
+ operationId: $sourceDescriptions.clinic.bookAppointment
118
+ - stepId: notify
119
+ operationId: $sourceDescriptions.clinic.sendConfirmation
120
+ ```
121
+
122
+ ## Smoke test (MCP over Streamable HTTP)
123
+
124
+ ```bash
125
+ hapi serve --specs ./clinic.openapi.yaml --capability-graph --capability-planning --dev --port 3000
126
+ ```
127
+
128
+ Then, with any MCP client (or curl with `initialize` → `notifications/initialized` → `tools/call`):
129
+
130
+ - `tool_search` `{ "query": "<a real user sentence in the users' language>" }` → expect the right operations in the top results.
131
+ - `capability_context` `{ "query": "<same sentence>" }` → expect the goal (`appointment.booked`) and `clientEstablished: true` on client facts.
132
+ - `capability_plan` `{ "initialFacts": [], "goalFacts": [{ "predicate": "appointment.booked" }] }` → `clarification-required` listing the missing client facts.
133
+
134
+ Search matches words: if English phrasing returns nothing for a Spanish API,
135
+ add the users' terms to summaries/descriptions and test again.
@@ -0,0 +1,90 @@
1
+ # Extension rules (as enforced by the HAPI compiler)
2
+
3
+ Malformed **supported** extensions fail `hapi graph compile` and graph-mode
4
+ `hapi serve` startup with a pointer-scoped diagnostic. Missing semantics are
5
+ inferred deterministically from OpenAPI (tags, summary, method, security,
6
+ Links) and keep provenance; explicit metadata always wins.
7
+
8
+ ## Operation extensions
9
+
10
+ | Extension | Shape | Diagnostics |
11
+ | --- | --- | --- |
12
+ | `x-hapi-capability` | non-empty string, or non-empty array of non-empty strings | `invalid-capability` |
13
+ | `x-hapi-intent` | object with only `use-when` and `avoid-when`, each an array of non-empty strings | `invalid-intent` |
14
+ | `x-hapi-effects` | object: `mode` (required) ∈ `read, create, update, delete, action`; optional booleans `side-effecting`, `destructive`, `idempotent`, `requires-confirmation`; no other keys | `invalid-effects` |
15
+ | `x-hapi-prerequisites` | array of non-empty strings | `invalid-prerequisites` |
16
+ | `x-hapi-planning` | object with only `requires`, `produces`, `removes`, `cost` | `invalid-planning`, `invalid-planning-field`, `invalid-planning-produces`, `invalid-planning-facts`, `invalid-planning-fact`, `duplicate-planning-fact`, `invalid-planning-cost`, `conflicting-planning-fact` |
17
+
18
+ `mode: write` is **not** valid (use `create`, `update`, or `action`).
19
+
20
+ ### Effect defaults by mode (when a boolean is omitted)
21
+
22
+ | mode | side-effecting | destructive | idempotent | requires-confirmation |
23
+ | --- | --- | --- | --- | --- |
24
+ | read | false | false | true | false |
25
+ | create | true | false | false | false |
26
+ | update | true | false | false | false |
27
+ | delete | true | true | true | true |
28
+ | action | true | false | false | false |
29
+
30
+ Without `x-hapi-effects`, the method decides: GET/HEAD/OPTIONS → read, POST →
31
+ create, DELETE → delete, PUT/PATCH → update (PUT idempotent).
32
+
33
+ ## Facts
34
+
35
+ - Shape: `{ predicate: string, value?: JSON }`, no other keys.
36
+ - Predicate pattern: `^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$`.
37
+ - Values must be JSON-safe (no functions, cycles, non-finite numbers).
38
+ - No duplicate fact in a list; nothing both produced and removed.
39
+ - `produces` must be non-empty. `cost` defaults to 1, finite and ≥ 0.
40
+ - Compiler-generated `operation.<id>.completed` facts exist; do not author them.
41
+
42
+ ## Document root
43
+
44
+ `x-hapi-client-facts`: array of `{ predicate, value?, description? }` with the
45
+ fact rules (`invalid-client-facts`). A declared fact no transition requires
46
+ compiles with an `unused-client-fact` warning.
47
+
48
+ ## Responses
49
+
50
+ `x-hapi-outcome` on a response object: only `produces` and/or `removes` (at least
51
+ one non-empty, same fact rules, no conflicts). Status key: code, `NXX`, or
52
+ `default`; never `2xx` (`outcome-on-success-response`). Needs operation
53
+ `x-hapi-planning`, otherwise ignored with `outcome-without-planning`.
54
+ Diagnostics: `invalid-outcome`, `invalid-outcome-field`, `empty-outcome`,
55
+ `conflicting-outcome-fact`. The planner never plans through an outcome; plan
56
+ steps list them, flow cards show them as "Otherwise", audit lists them as
57
+ response facts.
58
+
59
+ ## Webhooks and callbacks
60
+
61
+ `x-hapi-event: { produces: [facts] }` (non-empty) on OpenAPI 3.1 `webhooks` or
62
+ operation `callbacks` only. On `paths` operations it is ignored
63
+ (`event-on-path-operation`); unresolvable references are skipped
64
+ (`unresolved-trigger`). Triggers are descriptive: HAPI never subscribes or plans
65
+ through them.
66
+
67
+ ## MCP tool hints (HAPI core)
68
+
69
+ `x-readOnlyHint`, `x-destructiveHint`, `x-idempotentHint`, `x-openWorldHint`
70
+ (boolean or "true"/"false"). If none is set: GET → read-only, non-destructive;
71
+ POST/PUT/PATCH/DELETE → not read-only, destructive. `x-openWorldHint` defaults
72
+ to true in `--headless` mode unless `--no-open-world`.
73
+
74
+ ## Arazzo workflow planning
75
+
76
+ `x-hapi-planning` on a workflow uses the same shape. Not eligible (warning
77
+ `unsupported-arazzo-planning-workflow`) when the workflow has no resolved
78
+ OpenAPI steps, invokes nested workflows, or uses dynamic success/failure
79
+ control flow. Consistency warnings (failures under `hapi arazzo validate --strict`):
80
+ steps without valid planning, step requirements the workflow does not require,
81
+ step productions the workflow does not declare, declared removals that steps
82
+ leave present. Gates come from step security, destructive effects, and
83
+ `requires-confirmation`.
84
+
85
+ ## Gates and plan outcomes
86
+
87
+ Gates: `operation-authorization` (from security), `mutation-confirmation`
88
+ (destructive or requires-confirmation). Outcomes: `complete`, `planned`,
89
+ `clarification-required`, `unplannable`. Audit exit codes: 0 clean, 1 gaps,
90
+ 2 usage/compile error.
@@ -0,0 +1,52 @@
1
+ # How HAPI and OrcA use each piece of the contract
2
+
3
+ The contract is the program. HAPI reads it at startup and serves MCP tools from
4
+ it; there is no glue code to write. Every field below changes what the runtime
5
+ does, so write each one for its runtime effect.
6
+
7
+ ## HAPI runtime (`hapi serve`)
8
+
9
+ | Contract element | Runtime effect |
10
+ | --- | --- |
11
+ | `operationId` | MCP tool name; stable identity in the capability graph, plans, and OrcA Traffic. |
12
+ | `summary`, `description` | Tool description; matched word by word by `tool_search` and graph discovery. |
13
+ | parameters, request body, schemas | Tool `inputSchema`; HAPI validates arguments and builds the upstream HTTP request from them. Descriptions tell the model where each value comes from. |
14
+ | responses (+ descriptions) | What the tool returns and how errors read to the model; response descriptions are the model's instructions for the next turn. |
15
+ | `security` | Upstream authentication, and the `operation-authorization` gate; authorization filtering happens before any ranking. |
16
+ | `servers` / `--url` | Where calls go (`--headless --url` for an existing backend). |
17
+ | `x-readOnlyHint`, `x-destructiveHint`, `x-idempotentHint`, `x-openWorldHint` | MCP tool annotations. Without them: GET read-only, POST/PUT/PATCH/DELETE destructive. Clients decide confirmations from these. |
18
+ | `--deferred-tool-search` | Tools sent with `defer_loading: true`; a `tool_search` tool returns the relevant ones. Smaller context. |
19
+ | `--capability-graph` | Compiles the contract into the HAPI Capability Graph (implies deferred search with graph-aware ranking). Malformed `x-hapi-*` metadata fails startup. |
20
+ | `x-hapi-capability` | Taxonomy used for discovery and grouping. |
21
+ | `x-hapi-intent` | `use-when`/`avoid-when` evidence for ranking and disambiguation in context results. |
22
+ | `x-hapi-effects` | Effect mode and flags; destructive or `requires-confirmation` add the `mutation-confirmation` gate. |
23
+ | `x-hapi-prerequisites` | Human-readable readiness shown in context; never machine state. |
24
+ | `x-hapi-planning` | A planning transition: requires/produces/removes facts with a cost. The only way (with valid Links and eligible Arazzo workflows) to create one; HAPI never infers transitions from prose or verbs. |
25
+ | `x-hapi-client-facts` | Requirement cards marked `clientEstablished: true` in `capability_context`; audit treats them as established outside the API. Never makes a fact true. |
26
+ | `x-hapi-outcome` | Listed on plan steps as alternatives ("Otherwise" in flow cards); the client re-plans after verifying the response. |
27
+ | webhooks/callbacks + `x-hapi-event` | Descriptive triggers; HAPI never subscribes. |
28
+ | OpenAPI Links (fully mapped) | Planning-eligible transitions between operations; never map a search result into a selection input. |
29
+ | Arazzo workflows (+ `x-hapi-planning`) | One MCP tool per workflow that runs every step; eligible workflows become one atomic planning transition. |
30
+ | `--capability-planning` | Adds read-only `capability_context` (request → candidate goals, facts, gates, tools) and `capability_plan` (verified facts + goal → steps, missing facts, unsatisfied gates). Neither calls the API. |
31
+ | `--runtime-authority guarded` | Adds plan execution with revalidation and trusted confirmation receipts (advanced, opt-in). |
32
+
33
+ ## OrcA (VS Code harness for MCP and API work)
34
+
35
+ | Contract element | What OrcA does with it |
36
+ | --- | --- |
37
+ | `tool_search` + `defer_loading` | Progressive discovery: sends a `search_tools` tool and always-loaded tools; loads deferred tools per message and on demand. |
38
+ | tool names + first description sentence | A catalog of not-yet-loaded tools inside `search_tools` (names, ~80-char purpose), so the model knows they exist. |
39
+ | `capability_context` | Asked with each user message; results fill the **Context** tab and the tools they name are loaded automatically. |
40
+ | `capability_plan` | **Plan This Goal** in the Context tab with the facts the user verified. |
41
+ | read-only hint | Default confirmation policy asks before any tool that is not read-only. |
42
+ | response descriptions, outcomes | Shown in chat and Traffic; guide the next question. |
43
+ | `operationId` | **Open Operation in Contract** jumps from a Traffic frame to the operation. |
44
+
45
+ ## Consequences for authors
46
+
47
+ - A vague summary hides a tool from search; the agent then says it "has no access".
48
+ - A missing read-only hint on a POST search makes every search ask for confirmation.
49
+ - A requirement nobody produces and nobody declares as a client fact makes every
50
+ plan for that goal `clarification-required`.
51
+ - A search that "produces" a selection lets the agent pick for the user.
52
+ - A 409 without an outcome leaves the agent retrying instead of asking again.
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Static checks for intent-based HAPI metadata in an OpenAPI 3.x document.
4
+ * Complements (does not replace) `hapi graph compile` and `hapi graph audit`.
5
+ * Usage: bun check-intent-metadata.ts <openapi.yaml|json> [--json]
6
+ * Exit 1 on errors.
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+
10
+ type Level = 'error' | 'warning' | 'info';
11
+ interface Finding { level: Level; where: string; rule: string; message: string }
12
+ type Obj = Record<string, any>;
13
+
14
+ const METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
15
+ const MODES = new Set(['read', 'create', 'update', 'delete', 'action']);
16
+ const EFFECT_KEYS = new Set(['mode', 'side-effecting', 'destructive', 'idempotent', 'requires-confirmation']);
17
+ const PREDICATE = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
18
+ const COMMAND_LIKE = /^(get|list|create|update|delete|book|cancel|search|find|add|remove|send|post|put)[A-Z_.]/;
19
+ const WISH = /\b(wants|wishes|intends|desires|requested)\b/;
20
+
21
+ function load(file: string): Obj {
22
+ const text = readFileSync(file, 'utf8');
23
+ if (/\.json$/i.test(file)) return JSON.parse(text);
24
+ const yaml = (Bun as any).YAML;
25
+ if (!yaml?.parse) throw new Error('This Bun version has no YAML parser; convert the document to JSON first.');
26
+ return yaml.parse(text);
27
+ }
28
+
29
+ const isObj = (v: unknown): v is Obj => !!v && typeof v === 'object' && !Array.isArray(v);
30
+ const nonEmptyStrings = (v: unknown) => Array.isArray(v) && v.every((s) => typeof s === 'string' && s.trim() !== '');
31
+
32
+ function checkFacts(list: unknown, where: string, add: (l: Level, w: string, r: string, m: string) => void): string[] {
33
+ if (list === undefined) return [];
34
+ if (!Array.isArray(list)) {
35
+ add('error', where, 'facts', 'Facts must be an array.');
36
+ return [];
37
+ }
38
+ const keys: string[] = [];
39
+ for (const fact of list) {
40
+ if (!isObj(fact) || typeof fact.predicate !== 'string' || Object.keys(fact).some((k) => k !== 'predicate' && k !== 'value' && k !== 'description')) {
41
+ add('error', where, 'fact-shape', 'Each fact is { predicate, value? }.');
42
+ continue;
43
+ }
44
+ if (!PREDICATE.test(fact.predicate)) add('error', where, 'predicate', `"${fact.predicate}" must match ${PREDICATE}.`);
45
+ if (COMMAND_LIKE.test(fact.predicate)) add('warning', where, 'fact-is-command', `"${fact.predicate}" looks like a command; use business state.`);
46
+ if (WISH.test(fact.predicate.replace(/[._-]/g, ' '))) add('warning', where, 'fact-is-wish', `"${fact.predicate}" looks like a wish, not verified state.`);
47
+ const key = JSON.stringify([fact.predicate, fact.value]);
48
+ if (keys.includes(key)) add('error', where, 'duplicate-fact', `Duplicate fact "${fact.predicate}".`);
49
+ keys.push(key);
50
+ }
51
+ return list.filter(isObj).map((f) => f.predicate as string);
52
+ }
53
+
54
+ function checkOperation(op: Obj, method: string, path: string, add: (l: Level, w: string, r: string, m: string) => void, state: { produced: Set<string>; required: Map<string, string[]>; ids: Map<string, string> }) {
55
+ const where = `${method.toUpperCase()} ${path}`;
56
+ const id: string | undefined = op.operationId;
57
+ if (!id) add('error', where, 'operationId', 'Missing operationId (it becomes the tool name).');
58
+ else if (state.ids.has(id)) add('error', where, 'operationId', `Duplicate operationId "${id}" (also ${state.ids.get(id)}).`);
59
+ else state.ids.set(id, where);
60
+ const label = id ?? where;
61
+ if (!op.summary?.trim()) add('error', label, 'summary', 'Missing summary (drives search and the tool catalog).');
62
+ else if (op.summary.trim().split(/\s+/).length < 3) add('warning', label, 'summary', `Summary "${op.summary}" is too generic for search.`);
63
+ if (!op.description?.trim()) add('warning', label, 'description', 'No description: say what it does and where inputs come from.');
64
+ for (const p of op.parameters ?? []) if (isObj(p) && !p.$ref && !p.description) add('warning', label, 'parameter', `Parameter "${p.name}" has no description of its source.`);
65
+
66
+ const effects = op['x-hapi-effects'];
67
+ if (effects === undefined) add('warning', label, 'effects', 'No x-hapi-effects; the method decides (POST = create).');
68
+ else if (!isObj(effects) || !MODES.has(effects.mode) || Object.keys(effects).some((k) => !EFFECT_KEYS.has(k)) || Object.entries(effects).some(([k, v]) => k !== 'mode' && typeof v !== 'boolean')) {
69
+ add('error', label, 'effects', 'x-hapi-effects needs mode ∈ read|create|update|delete|action and only boolean flags.');
70
+ }
71
+ const readMode = isObj(effects) && effects.mode === 'read';
72
+ const hintRead = op['x-readOnlyHint'] === true || op['x-readOnlyHint'] === 'true';
73
+ if (readMode && method !== 'get' && method !== 'head' && !hintRead) add('error', label, 'tool-hint', `Read-only ${method.toUpperCase()} without x-readOnlyHint: true; clients will ask for confirmation on every call.`);
74
+ if (isObj(effects) && effects.mode === 'create' && method === 'post' && op['x-destructiveHint'] === undefined) add('warning', label, 'tool-hint', 'Create via POST is annotated destructive by default; set x-destructiveHint: false if nothing is lost.');
75
+
76
+ const cap = op['x-hapi-capability'];
77
+ if (cap === undefined) add('warning', label, 'capability', 'No x-hapi-capability taxonomy.');
78
+ else if (!(typeof cap === 'string' && cap.trim()) && !(nonEmptyStrings(cap) && cap.length)) add('error', label, 'capability', 'x-hapi-capability must be a non-empty string or array.');
79
+ const intent = op['x-hapi-intent'];
80
+ if (intent === undefined) add('warning', label, 'intent', 'No x-hapi-intent use-when/avoid-when.');
81
+ else if (!isObj(intent) || Object.keys(intent).some((k) => k !== 'use-when' && k !== 'avoid-when') || (intent['use-when'] && !nonEmptyStrings(intent['use-when'])) || (intent['avoid-when'] && !nonEmptyStrings(intent['avoid-when']))) add('error', label, 'intent', 'x-hapi-intent accepts only use-when/avoid-when string arrays.');
82
+ const pre = op['x-hapi-prerequisites'];
83
+ if (pre !== undefined && !nonEmptyStrings(pre)) add('error', label, 'prerequisites', 'x-hapi-prerequisites must be an array of non-empty strings.');
84
+ if (op['x-hapi-event'] !== undefined) add('warning', label, 'event', 'x-hapi-event on a paths operation is ignored; use webhooks/callbacks.');
85
+
86
+ const planning = op['x-hapi-planning'];
87
+ if (planning === undefined) {
88
+ add('info', label, 'planning', 'No x-hapi-planning: discovery-only operation.');
89
+ } else if (!isObj(planning) || Object.keys(planning).some((k) => !['requires', 'produces', 'removes', 'cost'].includes(k))) {
90
+ add('error', label, 'planning', 'x-hapi-planning accepts only requires, produces, removes, cost.');
91
+ } else {
92
+ const requires = checkFacts(planning.requires, label, add);
93
+ const produces = checkFacts(planning.produces, label, add);
94
+ const removes = checkFacts(planning.removes, label, add);
95
+ if (!produces.length) add('error', label, 'planning-produces', 'produces must be non-empty.');
96
+ if (planning.cost !== undefined && (typeof planning.cost !== 'number' || !Number.isFinite(planning.cost) || planning.cost < 0)) add('error', label, 'planning-cost', 'cost must be finite and ≥ 0.');
97
+ for (const p of produces) if (removes.includes(p)) add('error', label, 'conflicting-fact', `"${p}" is both produced and removed.`);
98
+ if (readMode) for (const p of produces) if (/\.selected$/.test(p)) add('error', label, 'search-selects', `A read operation produces "${p}"; selections are a person's choice (client fact).`);
99
+ if (isObj(effects) && effects.mode === 'delete' && !removes.length) add('warning', label, 'removes', 'Delete operation removes no fact; does it end some state?');
100
+ produces.forEach((p) => state.produced.add(p));
101
+ requires.forEach((r) => state.required.set(r, [...(state.required.get(r) ?? []), label]));
102
+ }
103
+ for (const [status, response] of Object.entries(op.responses ?? {})) {
104
+ if (!isObj(response) || response['x-hapi-outcome'] === undefined) continue;
105
+ const outcome = response['x-hapi-outcome'];
106
+ const at = `${label} ${status}`;
107
+ if (/^2/.test(status)) add('error', at, 'outcome-on-success', 'x-hapi-outcome is not allowed on 2xx responses.');
108
+ if (planning === undefined) add('warning', at, 'outcome-without-planning', 'Ignored: the operation has no x-hapi-planning.');
109
+ if (!isObj(outcome) || Object.keys(outcome).some((k) => k !== 'produces' && k !== 'removes')) {
110
+ add('error', at, 'outcome', 'x-hapi-outcome accepts only produces/removes.');
111
+ continue;
112
+ }
113
+ const p = checkFacts(outcome.produces, at, add);
114
+ const r = checkFacts(outcome.removes, at, add);
115
+ if (!p.length && !r.length) add('error', at, 'empty-outcome', 'Outcome needs produces or removes.');
116
+ p.forEach((f) => state.produced.add(f));
117
+ }
118
+ }
119
+
120
+ export function check(doc: Obj) {
121
+ const findings: Finding[] = [];
122
+ const add = (level: Level, where: string, rule: string, message: string) => findings.push({ level, where, rule, message });
123
+ const state = { produced: new Set<string>(), required: new Map<string, string[]>(), ids: new Map<string, string>() };
124
+ if (!String(doc.openapi ?? '').startsWith('3.')) add('error', 'document', 'openapi', 'Expected an OpenAPI 3.x document.');
125
+ for (const [path, item] of Object.entries(doc.paths ?? {})) {
126
+ if (!isObj(item)) continue;
127
+ for (const method of METHODS) if (isObj(item[method])) checkOperation(item[method], method, path, add, state);
128
+ }
129
+ for (const [name, item] of Object.entries(doc.webhooks ?? {})) {
130
+ if (!isObj(item)) continue;
131
+ for (const method of METHODS) {
132
+ const ev = item[method]?.['x-hapi-event'];
133
+ if (ev === undefined) continue;
134
+ const facts = isObj(ev) ? checkFacts(ev.produces, `webhook ${name}`, add) : [];
135
+ if (!facts.length) add('error', `webhook ${name}`, 'event', 'x-hapi-event.produces must be a non-empty fact array.');
136
+ facts.forEach((f) => state.produced.add(f));
137
+ }
138
+ }
139
+ const clientFacts = checkFacts(doc['x-hapi-client-facts'], 'x-hapi-client-facts', add);
140
+ for (const fact of doc['x-hapi-client-facts'] ?? []) if (isObj(fact) && !fact.description) add('warning', 'x-hapi-client-facts', 'client-fact-description', `"${fact.predicate}" has no description of who establishes it.`);
141
+ for (const fact of clientFacts) if (!state.required.has(fact)) add('warning', 'x-hapi-client-facts', 'unused-client-fact', `"${fact}" is not required by any operation.`);
142
+ for (const [fact, by] of state.required) {
143
+ if (!state.produced.has(fact) && !clientFacts.includes(fact)) add('error', fact, 'orphan-requirement', `Required by ${by.join(', ')} but never produced and not a client fact.`);
144
+ }
145
+ return { findings, operations: state.ids.size };
146
+ }
147
+
148
+ if (import.meta.main) {
149
+ const file = process.argv[2];
150
+ if (!file) {
151
+ console.error('Usage: bun check-intent-metadata.ts <openapi.yaml|json> [--json]');
152
+ process.exit(2);
153
+ }
154
+ const { findings, operations } = check(load(file));
155
+ if (process.argv.includes('--json')) console.log(JSON.stringify({ operations, findings }, null, 2));
156
+ else {
157
+ const icon = { error: '✗', warning: '!', info: '·' } as const;
158
+ for (const f of findings) console.log(`${icon[f.level]} [${f.rule}] ${f.where}: ${f.message}`);
159
+ const count = (l: Level) => findings.filter((f) => f.level === l).length;
160
+ console.log(`${operations} operation(s) · ${count('error')} error(s) · ${count('warning')} warning(s) · ${count('info')} info`);
161
+ }
162
+ process.exit(findings.some((f) => f.level === 'error') ? 1 : 0);
163
+ }
@@ -7,4 +7,4 @@
7
7
  * third-party plugins should not appear — those are discovered through
8
8
  * `$HAPI_HOME/config.json#plugins` or the known-plugins registry.
9
9
  */
10
- export declare const WORKSPACE_PLUGINS: readonly ["@mcp-com-ai/plugin-openapi", "@mcp-com-ai/plugin-arazzo", "@mcp-com-ai/plugin-cloudflare", "@mcp-com-ai/plugin-otel"];
10
+ export declare const WORKSPACE_PLUGINS: readonly ["@mcp-com-ai/plugin-openapi", "@mcp-com-ai/plugin-arazzo", "@mcp-com-ai/plugin-cloudflare", "@mcp-com-ai/plugin-otel", "@mcp-com-ai/plugin-capability-graph"];