agent-tool-forge 0.4.7 → 0.4.9
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/lib/config-schema.js +3 -3
- package/lib/forge-service.js +7 -4
- package/lib/hitl-engine.d.ts +3 -3
- package/lib/index.js +2 -3
- package/lib/init.js +1 -1
- package/lib/sidecar.d.ts +4 -2
- package/package.json +2 -1
- package/skills/forge-eval/SKILL.md +69 -0
- package/skills/forge-eval/references/assertion-patterns.md +265 -0
- package/skills/forge-eval/references/eval-types.md +262 -0
- package/skills/forge-eval/references/overlap-map.md +89 -0
- package/skills/forge-mcp/SKILL.md +62 -0
- package/skills/forge-mcp/references/mcp-templates.md +302 -0
- package/skills/forge-mcp/references/tool-to-mcp-mapping.md +108 -0
- package/skills/forge-tool/SKILL.md +112 -0
- package/skills/forge-tool/references/description-contract.md +102 -0
- package/skills/forge-tool/references/extension-points.md +120 -0
- package/skills/forge-tool/references/pending-spec.md +53 -0
- package/skills/forge-tool/references/tool-shape.md +106 -0
- package/skills/forge-verifier/SKILL.md +78 -0
- package/skills/forge-verifier/references/output-groups.md +39 -0
- package/skills/forge-verifier/references/verifier-pattern.md +83 -0
- package/skills/forge-verifier/references/verifier-stubs.md +147 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# Eval Case Interfaces
|
|
2
|
+
|
|
3
|
+
## GoldenEvalCase
|
|
4
|
+
|
|
5
|
+
Sends a known prompt through the full agent loop. Asserts on single-tool selection + response content. The routing sanity check.
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
GoldenEvalCase {
|
|
9
|
+
id: string // Format: "gs-<toolname>-NNN"
|
|
10
|
+
description: string // What this case tests
|
|
11
|
+
input: {
|
|
12
|
+
message: string // The user prompt to send
|
|
13
|
+
}
|
|
14
|
+
stubs?: {
|
|
15
|
+
[toolName: string]: object // Stub response for each tool (activates multi-turn mode)
|
|
16
|
+
}
|
|
17
|
+
maxTurns?: number // Max LLM loop iterations in stub mode (default 5)
|
|
18
|
+
expect: {
|
|
19
|
+
toolsCalled: string[] // Exact tools that must be called
|
|
20
|
+
noToolErrors: boolean // In stub mode: all tools must have stub entries
|
|
21
|
+
responseNonEmpty: boolean // Agent produced a response
|
|
22
|
+
responseContains?: string[] // ALL must appear (exact values)
|
|
23
|
+
responseContainsAny?: string[][] // At least one from EACH group
|
|
24
|
+
responseNotContains?: string[] // NONE may appear
|
|
25
|
+
maxLatencyMs?: number // Response time budget
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Field Semantics
|
|
31
|
+
|
|
32
|
+
- **toolsCalled** — Exact match. If you expect `["get_weather"]`, the agent must call exactly `get_weather` and no other tools.
|
|
33
|
+
- **stubs** — Map of tool name → stub return object. When present, activates stub-based multi-turn mode: the runner feeds stub results back to the model after each tool call and continues until the model produces a final text response. Response assertions (`responseContains`, etc.) check the **final** response, not first-turn text. Required for `noToolErrors` and response content assertions to be meaningful.
|
|
34
|
+
- **maxTurns** — Safety cap on the stub-based loop. Defaults to 5. The runner stops after this many LLM turns even if the model keeps calling tools.
|
|
35
|
+
- **noToolErrors** — In stub mode: fails if the model calls a tool that has no entry in `stubs`. In routing-only mode (no `stubs`): no-op.
|
|
36
|
+
- **responseContains** — Substring match, case-sensitive. Use for exact values that prove the tool returned real data (dollar amounts, names, IDs). Meaningful only in stub mode.
|
|
37
|
+
- **responseContainsAny** — Each inner array is a synonym group. At least one member from each group must appear. Use for domain terms where multiple phrasings are acceptable.
|
|
38
|
+
- **responseNotContains** — Substring match, case-sensitive. Use to catch: cop-outs ("I don't know"), raw JSON leaks ("fetchedAt"), sensitive data ("API_KEY").
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## LabeledEvalCase
|
|
43
|
+
|
|
44
|
+
Sends a message through the full agent loop. Tests tool routing under ambiguity, edge cases, and adversarial inputs.
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
LabeledEvalCase {
|
|
48
|
+
id: string // Format: "ls-<toolname>-NNN"
|
|
49
|
+
description: string // What this case tests
|
|
50
|
+
difficulty: 'straightforward' | 'ambiguous' | 'edge'
|
|
51
|
+
input: {
|
|
52
|
+
message: string // The user prompt to send
|
|
53
|
+
}
|
|
54
|
+
expect: {
|
|
55
|
+
// Tool ROUTING assertions (use one of toolsCalled OR toolsAcceptable)
|
|
56
|
+
toolsCalled?: string[] // Exact tools that must appear
|
|
57
|
+
toolsAcceptable?: string[][] // Any of these tool sets is valid
|
|
58
|
+
toolsNotCalled?: string[] // Tools that must NOT be called
|
|
59
|
+
|
|
60
|
+
noToolErrors?: boolean
|
|
61
|
+
|
|
62
|
+
// Parameter assertions (did the model pass the right arguments?)
|
|
63
|
+
toolParams?: ToolParamAssertion[]
|
|
64
|
+
|
|
65
|
+
// Response quality assertions (all deterministic)
|
|
66
|
+
responseNonEmpty?: boolean
|
|
67
|
+
responseContains?: string[]
|
|
68
|
+
responseContainsAny?: string[][]
|
|
69
|
+
responseNotContains?: string[]
|
|
70
|
+
responseMatches?: string[] // Regex patterns
|
|
71
|
+
maxLatencyMs?: number
|
|
72
|
+
maxTokens?: number // Response token count ceiling
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### toolsCalled vs toolsAcceptable
|
|
78
|
+
|
|
79
|
+
- **toolsCalled** — Use for straightforward cases where the exact tool set is known. `["get_weather", "get_forecast"]` means both must be called.
|
|
80
|
+
- **toolsAcceptable** — Use for ambiguous cases where multiple strategies are valid. `[["get_weather"], ["get_weather", "get_forecast"]]` means either set is acceptable.
|
|
81
|
+
- Never use both on the same case.
|
|
82
|
+
- For edge cases where no tools should be called, use `toolsAcceptable: [["__none__"]]`.
|
|
83
|
+
|
|
84
|
+
### Difficulty Tiers
|
|
85
|
+
|
|
86
|
+
- **straightforward** — Clear multi-tool tasks. Obvious which tools to use and in what combination.
|
|
87
|
+
- **ambiguous** — Multiple valid interpretations or tool combinations. Tests the agent's judgment.
|
|
88
|
+
- **edge** — Adversarial inputs, prompt injection, off-topic, contradictions. Tests robustness.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## ToolParamAssertion
|
|
93
|
+
|
|
94
|
+
Asserts that the model passed correct arguments to a tool call. This catches a failure class that routing assertions miss: the model calls the right tool but passes wrong, missing, or hallucinated parameters.
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
ToolParamAssertion {
|
|
98
|
+
tool: string // Which tool call to check
|
|
99
|
+
paramName: string // Which parameter to assert on
|
|
100
|
+
assertion: 'equals' | 'contains' | 'oneOf' | 'exists' | 'notExists' | 'matches'
|
|
101
|
+
value?: string | string[] // Expected value(s) — not needed for exists/notExists
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Assertion Types
|
|
106
|
+
|
|
107
|
+
- **equals** — Exact match (case-sensitive). `{ tool: "get_weather", paramName: "city", assertion: "equals", value: "Paris" }`
|
|
108
|
+
- **contains** — Substring match. `{ paramName: "query", assertion: "contains", value: "weather" }`
|
|
109
|
+
- **oneOf** — Value matches any in the list. `{ paramName: "units", assertion: "oneOf", value: ["metric", "imperial"] }`
|
|
110
|
+
- **exists** — Parameter was provided (any value). `{ paramName: "city", assertion: "exists" }`
|
|
111
|
+
- **notExists** — Parameter was NOT provided (catches hallucinated params). `{ paramName: "country_code", assertion: "notExists" }`
|
|
112
|
+
- **matches** — Regex match. `{ paramName: "date", assertion: "matches", value: "^\\d{4}-\\d{2}-\\d{2}$" }`
|
|
113
|
+
|
|
114
|
+
### When to Use
|
|
115
|
+
|
|
116
|
+
- **Golden evals:** Assert the obvious parameter. "What's the weather in Paris?" → `city` equals `"Paris"` (or contains `"Paris"`).
|
|
117
|
+
- **Labeled straightforward:** Assert parameters across multiple tool calls. "Weather and air quality in Beijing" → `get_weather.city` contains `"Beijing"` AND `get_air_quality.city` contains `"Beijing"`.
|
|
118
|
+
- **Labeled ambiguous:** Use `oneOf` for parameters with valid alternatives. Units could be `"metric"` or `"imperial"` depending on user locale.
|
|
119
|
+
- **Edge cases:** Use `notExists` to catch hallucinated parameters the schema doesn't define.
|
|
120
|
+
|
|
121
|
+
### Rules
|
|
122
|
+
|
|
123
|
+
- Parameter assertions check the arguments the model SENT to the tool, not the tool's response.
|
|
124
|
+
- Use `contains` over `equals` when the model might normalize the input (e.g., `"Paris"` vs `"Paris, FR"` vs `"paris"`).
|
|
125
|
+
- Use `oneOf` when multiple values are acceptable (enum fields, locale-dependent defaults).
|
|
126
|
+
- Use template tokens (`{{seed:*}}`) in values for data-dependent parameters.
|
|
127
|
+
- If the tool wasn't called (routing assertion already failed), parameter assertions are skipped.
|
|
128
|
+
- When `toolsAcceptable` is used, parameter assertions apply only if the tool was actually called.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## RegressionEvalCase
|
|
133
|
+
|
|
134
|
+
Captures a specific bug that was found and fixed. Immutable once created — never modified, only archived when no longer applicable.
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
RegressionEvalCase {
|
|
138
|
+
id: string // Format: "rg-<toolname>-NNN"
|
|
139
|
+
description: string // "regression — <what the bug was>"
|
|
140
|
+
difficulty: 'regression'
|
|
141
|
+
createdAt: string // ISO 8601 — when this case was created
|
|
142
|
+
bugRef: string // Issue number or commit hash of the fix
|
|
143
|
+
input: {
|
|
144
|
+
message: string // The exact prompt that triggered the bug
|
|
145
|
+
}
|
|
146
|
+
expect: {
|
|
147
|
+
toolsCalled?: string[]
|
|
148
|
+
toolsAcceptable?: string[][]
|
|
149
|
+
noToolErrors?: boolean
|
|
150
|
+
responseNonEmpty?: boolean
|
|
151
|
+
responseContains?: string[] // Use seed-stable values only (no snapshots)
|
|
152
|
+
responseContainsAny?: string[][]
|
|
153
|
+
responseNotContains?: string[]
|
|
154
|
+
maxLatencyMs?: number
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Key Properties
|
|
160
|
+
|
|
161
|
+
- **Immutable** — Never edited after creation. If the tool changes so much the case no longer applies, archive it.
|
|
162
|
+
- **Cheap to run** — Seed-stable assertions only (no `{{snapshot:*}}` tokens). Suitable for CI.
|
|
163
|
+
- **Not scaled** — No formula. One case per real bug, created ad-hoc.
|
|
164
|
+
- **Self-documenting** — `bugRef` links to the fix, so a failure immediately tells you what regressed.
|
|
165
|
+
- **Separate file** — Stored in `<toolname>.regression.json`, separate from golden and labeled.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## RubricEvalCase (Stub)
|
|
170
|
+
|
|
171
|
+
Reserved for scored multi-dimensional evaluation. Use for synthesis quality testing where deterministic assertions are insufficient — e.g., "did the agent coherently combine data from 3 tools into a useful response?"
|
|
172
|
+
|
|
173
|
+
```
|
|
174
|
+
RubricEvalCase {
|
|
175
|
+
id: string
|
|
176
|
+
description: string
|
|
177
|
+
input: { message: string }
|
|
178
|
+
referenceData?: Record<string, unknown> // Ground truth tool output for the judge
|
|
179
|
+
rubric: {
|
|
180
|
+
dimension: string // e.g., "accuracy", "completeness", "safety"
|
|
181
|
+
maxScore: number // Use 1 for binary (pass/fail), 3-5 for graded
|
|
182
|
+
criteria: string // What each score level means
|
|
183
|
+
}[]
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### When to Use
|
|
188
|
+
|
|
189
|
+
- Multi-tool synthesis: "Did the response accurately integrate data from all called tools?"
|
|
190
|
+
- Response quality: "Is the response helpful and well-structured?" (not testable with substring matching)
|
|
191
|
+
- Safety nuance: "Did the response appropriately hedge on investment advice?"
|
|
192
|
+
|
|
193
|
+
### Implementation Notes
|
|
194
|
+
|
|
195
|
+
- Binary scoring (`maxScore: 1`) is more defensible than graded rubrics
|
|
196
|
+
- The `referenceData` field provides ground truth so the judge compares against real data, not its own knowledge
|
|
197
|
+
- Keep rubric evals in a separate tier — run deterministic evals first, rubric evals only when deterministic pass
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## EvalCaseResult
|
|
202
|
+
|
|
203
|
+
The result of running a single eval case.
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
EvalCaseResult {
|
|
207
|
+
id: string
|
|
208
|
+
description: string
|
|
209
|
+
passed: boolean
|
|
210
|
+
durationMs: number
|
|
211
|
+
error?: string // Failure reason
|
|
212
|
+
details?: Record<string, any> // Extra info: tools called, response, etc.
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## EvalSuiteResult
|
|
217
|
+
|
|
218
|
+
The result of running an entire eval suite.
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
EvalSuiteResult {
|
|
222
|
+
runId: string // UUID — unique identifier for this run
|
|
223
|
+
timestamp: string // ISO 8601
|
|
224
|
+
tier: 'golden' | 'labeled' | 'regression'
|
|
225
|
+
toolName: string
|
|
226
|
+
|
|
227
|
+
// Current state at run time (for staleness comparison)
|
|
228
|
+
metadata: {
|
|
229
|
+
toolVersion: string
|
|
230
|
+
descriptionHash: string // sha256(description)[0:12]
|
|
231
|
+
registrySize: number
|
|
232
|
+
evalFileHash: string // sha256(eval JSON file)[0:12]
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
stalenessWarnings: string[] // Warnings from staleness checks
|
|
236
|
+
|
|
237
|
+
cases: EvalCaseResult[]
|
|
238
|
+
|
|
239
|
+
summary: {
|
|
240
|
+
totalCases: number
|
|
241
|
+
passed: number
|
|
242
|
+
failed: number
|
|
243
|
+
skippedAssertions: number
|
|
244
|
+
totalDurationMs: number
|
|
245
|
+
estimatedCostUsd?: number
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Baseline diffing (optional — requires a previous runId)
|
|
249
|
+
baselineRunId?: string // Previous run to compare against
|
|
250
|
+
regressions: string[] // Case IDs that passed before, fail now
|
|
251
|
+
newPasses: string[] // Case IDs that failed before, pass now
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### Baseline Diffing
|
|
256
|
+
|
|
257
|
+
When `baselineRunId` is provided, the runner loads the previous `EvalSuiteResult` and compares:
|
|
258
|
+
- **Regression:** Passed in baseline, fails now → add to `regressions[]`
|
|
259
|
+
- **New pass:** Failed in baseline, passes now → add to `newPasses[]`
|
|
260
|
+
- **Unchanged:** No note
|
|
261
|
+
|
|
262
|
+
This is the minimum viable A/B comparison. It answers: "Did my change break anything that was working?"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Tool Overlap Map — Format and Usage
|
|
2
|
+
|
|
3
|
+
The overlap map declares which tools are **close neighbors** — tools that could plausibly be confused by a natural language prompt. It serves two purposes:
|
|
4
|
+
|
|
5
|
+
1. **Eval generation** — The eval factory reads the map to target ambiguous cases at real overlaps
|
|
6
|
+
2. **Coverage gap detection** — Cross-reference the map with existing evals to find untested overlaps
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Structure
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"get_weather": {
|
|
15
|
+
"overlaps": ["get_forecast", "get_air_quality"],
|
|
16
|
+
"clusters": [
|
|
17
|
+
["get_weather", "get_forecast", "get_air_quality"]
|
|
18
|
+
],
|
|
19
|
+
"reason": "Weather queries are broad — 'what's it like outside' could route to current conditions, forecast, or air quality"
|
|
20
|
+
},
|
|
21
|
+
"get_forecast": {
|
|
22
|
+
"overlaps": ["get_weather"],
|
|
23
|
+
"clusters": [
|
|
24
|
+
["get_weather", "get_forecast", "get_air_quality"]
|
|
25
|
+
],
|
|
26
|
+
"reason": "Future-oriented weather questions could route to forecast or current weather with extrapolation"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Fields
|
|
32
|
+
|
|
33
|
+
- **`overlaps`** — Pairwise close neighbors. Tools that could be confused 1:1.
|
|
34
|
+
- **`clusters`** — Groups of 3+ tools that frequently appear together in complex prompts. A cluster means "these tools naturally co-occur in multi-step tasks."
|
|
35
|
+
- **`reason`** — Human-readable explanation of why these tools overlap. Useful for onboarding and debugging.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Rules
|
|
40
|
+
|
|
41
|
+
### When to declare an overlap
|
|
42
|
+
Two tools overlap when a natural language prompt could reasonably route to either one.
|
|
43
|
+
- `get_weather` and `get_forecast` overlap on "what's the weather like"
|
|
44
|
+
- `get_weather` and `delete_account` do NOT overlap
|
|
45
|
+
|
|
46
|
+
### Clusters capture natural groupings
|
|
47
|
+
When a user asks a broad question ("tell me everything about the weather"), which tools naturally get called together? That's a cluster. Cluster members don't need to be pairwise confusable — they just co-occur in multi-step tasks.
|
|
48
|
+
|
|
49
|
+
### Sparse, not exhaustive
|
|
50
|
+
At 50 tools, most tools should have 2-4 overlaps and 0-2 clusters. Hub tools may have more. The map is NOT an NxN matrix.
|
|
51
|
+
|
|
52
|
+
### Symmetric by convention
|
|
53
|
+
If A lists B as an overlap, B should list A.
|
|
54
|
+
|
|
55
|
+
### The tool factory adds the entry
|
|
56
|
+
When a new tool is created, the factory identifies close neighbors and clusters, then updates the overlap map. The eval factory reads it.
|
|
57
|
+
|
|
58
|
+
### Coverage check surfaces gaps
|
|
59
|
+
A coverage check should report:
|
|
60
|
+
- (a) tools with no golden evals
|
|
61
|
+
- (b) tools with no labeled evals
|
|
62
|
+
- (c) declared overlaps with no ambiguous eval testing both tools together
|
|
63
|
+
- (d) declared clusters with no labeled eval exercising the full group
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## When NOT to Add an Overlap
|
|
68
|
+
|
|
69
|
+
- Tools in different categories serving obviously different purposes (read vs write)
|
|
70
|
+
- Tools whose descriptions have zero shared trigger conditions
|
|
71
|
+
- Tools where confusion would require a badly malformed prompt
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## Growth Management
|
|
76
|
+
|
|
77
|
+
As the registry scales, periodically review the map for stale entries. If two tools' descriptions have been refined to eliminate confusion, remove the overlap. The coverage check confirms they're no longer tested together — which is correct if they genuinely don't overlap.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## How the Eval Factory Uses the Map
|
|
82
|
+
|
|
83
|
+
1. **Read the map** before generating labeled evals
|
|
84
|
+
2. **Count overlaps (O) and clusters (C)** for the scaling formula
|
|
85
|
+
3. **Target ambiguous cases at declared overlaps** — every overlap pair gets at least one ambiguous eval
|
|
86
|
+
4. **Use clusters for multi-tool cases** — clusters drive 3-tool and 4+ tool labeled cases
|
|
87
|
+
5. **After generation**, verify every declared overlap has coverage
|
|
88
|
+
|
|
89
|
+
The map turns ambiguous eval generation from guesswork into a directed process.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# /forge-mcp — Generate MCP Server Scaffold
|
|
2
|
+
|
|
3
|
+
Generate a Model Context Protocol (MCP) server scaffold from one or more ToolDefinition objects. The generated server can be registered with any MCP-compatible client (Claude, Cursor, etc.).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Step 1 — Identify Tools to Expose
|
|
8
|
+
|
|
9
|
+
Ask the user which tools to expose via MCP, or default to all tools in `tools/index.js`.
|
|
10
|
+
|
|
11
|
+
For each tool, read its ToolDefinition to extract:
|
|
12
|
+
- `name`, `description`, `schema` (parameters)
|
|
13
|
+
- `mcpRouting` (HTTP endpoint and method)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Step 2 — Generate MCP Server
|
|
18
|
+
|
|
19
|
+
Generate `mcp-server.js` (or the user's preferred filename) using `@modelcontextprotocol/sdk`:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
23
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
24
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The server must:
|
|
28
|
+
1. Implement `tools/list` — return all tools with their JSON schemas
|
|
29
|
+
2. Implement `tools/call` — route to the correct ToolDefinition's `mcpRouting` HTTP endpoint
|
|
30
|
+
3. Handle auth headers from environment (`FORGE_API_KEY` or tool-specific env vars)
|
|
31
|
+
4. Return structured errors on tool failure
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Step 3 — Generate Transport Config
|
|
36
|
+
|
|
37
|
+
Generate a `mcp.json` config snippet the user can paste into their Claude Desktop or Cursor config:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"<project-name>": {
|
|
43
|
+
"command": "node",
|
|
44
|
+
"args": ["./mcp-server.js"],
|
|
45
|
+
"env": {
|
|
46
|
+
"API_BASE_URL": "http://localhost:3000",
|
|
47
|
+
"FORGE_API_KEY": "<your-api-key>"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Step 4 — Summary
|
|
57
|
+
|
|
58
|
+
Print:
|
|
59
|
+
- MCP server file path
|
|
60
|
+
- Number of tools exposed
|
|
61
|
+
- Config snippet location
|
|
62
|
+
- Instructions for registering with the MCP client
|