@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.
- package/README.md +6 -0
- package/dist/bin.js +121 -113
- package/dist/core/document-source.d.ts +4 -0
- package/dist/core/index.js +64 -64
- package/dist/core/plugin-loader.d.ts +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +122 -114
- package/dist/skills/hapi-federated-apis/SKILL.md +452 -0
- package/dist/skills/hapi-federated-apis/scripts/audit-planning-facts.ts +127 -0
- package/dist/skills/hapi-intent-based-apis/SKILL.md +199 -0
- package/dist/skills/hapi-intent-based-apis/references/examples.md +135 -0
- package/dist/skills/hapi-intent-based-apis/references/extension-rules.md +90 -0
- package/dist/skills/hapi-intent-based-apis/references/runtime-behavior.md +52 -0
- package/dist/skills/hapi-intent-based-apis/scripts/check-intent-metadata.ts +163 -0
- package/dist/workspace-plugins.d.ts +1 -1
- package/package.json +2 -1
|
@@ -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"];
|