fa-mcp-sdk 0.4.60 → 0.4.64
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/bin/fa-mcp.js +0 -9
- package/cli-template/.claude/skills/deploy-mcp/SKILL.md +440 -0
- package/cli-template/.claude/skills/deploy-mcp/scripts/check-openai.js +79 -0
- package/cli-template/.claude/skills/deploy-mcp/scripts/gen-secrets.js +116 -0
- package/cli-template/.claude/skills/deploy-mcp/scripts/gitlab-push.js +157 -0
- package/cli-template/.claude/skills/deploy-mcp/scripts/headless-chat.js +166 -0
- package/cli-template/.claude/skills/deploy-mcp/scripts/headless-test.js +110 -0
- package/cli-template/.claude/skills/readme-generator/reference/satellite-templates.md +1 -1
- package/cli-template/.claude/skills/readme-generator/reference/templates.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/01-getting-started.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/03-configuration.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +5 -5
- package/cli-template/package.json +1 -1
- package/cli-template/readme-docs/SKILLS.md +49 -0
- package/cli-template/tsconfig.json +8 -1
- package/config/_local.yaml +5 -5
- package/config/custom-environment-variables.yaml +1 -1
- package/config/default.yaml +5 -5
- package/config/local.yaml +3 -2
- package/dist/core/_types_/config.d.ts +1 -1
- package/dist/core/_types_/config.d.ts.map +1 -1
- package/dist/core/utils/formatToolResult.js +2 -2
- package/dist/core/utils/formatToolResult.js.map +1 -1
- package/package.json +1 -1
package/bin/fa-mcp.js
CHANGED
|
@@ -295,10 +295,6 @@ certificate's public and private keys`,
|
|
|
295
295
|
defaultValue: 'false',
|
|
296
296
|
name: 'isProduction',
|
|
297
297
|
},
|
|
298
|
-
{
|
|
299
|
-
skip: true,
|
|
300
|
-
name: 'NODE_ENV',
|
|
301
|
-
},
|
|
302
298
|
{
|
|
303
299
|
name: 'SERVICE_INSTANCE',
|
|
304
300
|
defaultValue: '',
|
|
@@ -709,11 +705,6 @@ certificate's public and private keys`,
|
|
|
709
705
|
await this.setLastConfigPath(config.projectAbsPath, config);
|
|
710
706
|
}
|
|
711
707
|
|
|
712
|
-
if (configProxy.NODE_ENV === 'development') {
|
|
713
|
-
configProxy.isProduction = 'false';
|
|
714
|
-
} else if (configProxy.NODE_ENV === 'production') {
|
|
715
|
-
configProxy.isProduction = 'true';
|
|
716
|
-
}
|
|
717
708
|
if (config['logger.useFileLogger'] !== 'true') {
|
|
718
709
|
config['logger.dir'] = '';
|
|
719
710
|
}
|
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: deploy-mcp
|
|
3
|
+
description: "Implement an fa-mcp MCP server end-to-end in this already-scaffolded project: verify Agent Tester OpenAI creds, seed dev-time secrets and lenient config, push the scaffold to GitLab (creating a new repo OR reusing an existing one when instructed), draft an implementation plan, implement tools/prompts/resources, iterate via the Agent Tester headless API, then push the finished work. Use when the user asks to develop/implement/deploy the MCP server in this project, mentions 'deploy-mcp', 'развернуть MCP', 'реализовать MCP', or supplies a feature brief."
|
|
4
|
+
disable-model-invocation: true
|
|
5
|
+
allowed-tools: Bash(node *), Bash(yarn *), Bash(npm *), Bash(git *), Bash(pwd), Bash(cd *), Bash(curl *), Read, Write, Edit, Glob, Grep
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Deploy MCP — feature implementation
|
|
9
|
+
|
|
10
|
+
Implement this MCP server against a feature brief, iteratively refine via the Agent Tester headless
|
|
11
|
+
API, and push the result to GitLab. The project has **already been scaffolded** by the `fa-mcp` CLI —
|
|
12
|
+
this skill picks up from the first `yarn install` and ends with the finished feature pushed to GitLab.
|
|
13
|
+
|
|
14
|
+
The skill makes **two GitLab pushes**: the first (Step 5) lands the scaffolded, configured project
|
|
15
|
+
on the remote so the rest of the work is tracked; the second (Step 10) pushes the implemented and
|
|
16
|
+
tested feature on top. In Step 5 the remote is either created via `gitlab-push.js` (default) or
|
|
17
|
+
reused as-is when the accompanying instructions say a repo already exists or `origin` is already
|
|
18
|
+
wired up — no duplicate projects get created.
|
|
19
|
+
|
|
20
|
+
All supporting scripts live in `${CLAUDE_SKILL_DIR}/scripts/` and are invoked with `node`.
|
|
21
|
+
|
|
22
|
+
## Ground rules
|
|
23
|
+
|
|
24
|
+
- **Every step is explicit and verified**. Do NOT silently skip a step. If a step fails, stop and report.
|
|
25
|
+
- **Never ask the user with predefined options for free-form input** (usernames, paths, tokens, keys,
|
|
26
|
+
URLs). Ask the question in plain prose; the user types the answer.
|
|
27
|
+
- **Respect exclusions from the accompanying text**. If it says "no AD" or "no Consul" — do NOT
|
|
28
|
+
ask for those creds and do NOT configure them.
|
|
29
|
+
- **Dev-time defaults are lenient on purpose** (auth off, Consul off, Agent Tester on). Production
|
|
30
|
+
config comes later; this skill is about getting the loop closed.
|
|
31
|
+
- **You are already inside the project root.** All paths are relative to the current working
|
|
32
|
+
directory unless stated otherwise. Use `pwd` once at the start to confirm.
|
|
33
|
+
- **Do not touch `.claude/`, `deploy/`, or `FA-MCP-SDK-DOC/`.** These directories are maintained
|
|
34
|
+
by the CLI / skill infrastructure and by the SDK maintainer. Do NOT modify, add, or delete files
|
|
35
|
+
inside them unless the accompanying text explicitly instructs you to. This applies to every step
|
|
36
|
+
below — implementation, tests, dev report, everything.
|
|
37
|
+
|
|
38
|
+
## Step 1 — Scan the accompanying text for requirements
|
|
39
|
+
|
|
40
|
+
Before touching code, read every message/file the user attached and extract:
|
|
41
|
+
|
|
42
|
+
- **Tool requirements** — what the MCP server must expose (tools, resources, prompts, REST endpoints).
|
|
43
|
+
- **Source-of-truth references** — existing code paths (e.g. "wrap the tools in `D:/foo/bar/`"),
|
|
44
|
+
public APIs to proxy, or other MCP projects to crib from. If a path is given, use Read/Glob/Grep
|
|
45
|
+
on it to understand the surface area before writing code. If an API is named, fetch its docs
|
|
46
|
+
(Context7 / WebFetch) before guessing at parameters.
|
|
47
|
+
- **Exclusions** — "no AD", "no Consul", "no DB", etc. Record them; do not ask for those creds later.
|
|
48
|
+
- **Additional creds required by the feature** (DB user/password, upstream service tokens, AD
|
|
49
|
+
service account, etc.). Ask for ONLY what the feature actually needs and nothing the text excluded.
|
|
50
|
+
- **Agent Tester OpenAI creds** — `apiKey` (required for Step 2) and `baseURL` (optional — Azure /
|
|
51
|
+
proxy / local LLM). If the text already supplies them, use those. If `config/local.yaml` already
|
|
52
|
+
has a working `agentTester.openAi.apiKey`, re-use it instead of asking again.
|
|
53
|
+
|
|
54
|
+
Summarize what you found to the user in 3-6 bullets and get a one-line confirmation before proceeding.
|
|
55
|
+
|
|
56
|
+
## Step 2 — Verify Agent Tester OpenAI credentials
|
|
57
|
+
|
|
58
|
+
A broken key uncovered after implementing, building, and starting the server is a very expensive
|
|
59
|
+
failure. Verify NOW, before anything else touches `config/local.yaml`:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
node ${CLAUDE_SKILL_DIR}/scripts/check-openai.js --key "<apiKey>" [--base-url "<baseURL>"]
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Exit code semantics:
|
|
66
|
+
- `0` — OK (2xx from `GET /v1/models`). Remember the creds and continue.
|
|
67
|
+
- `1` — key rejected (401/403). Tell the user, ask for a replacement, re-check. Do NOT continue.
|
|
68
|
+
- `2` — transport error (DNS/TLS/timeout). Likely wrong `baseURL` or offline — ask the user, re-check.
|
|
69
|
+
- `3` — unexpected HTTP status. Show the response body; some proxies don't implement `/v1/models`.
|
|
70
|
+
Let the user explicitly choose to proceed anyway (record the choice in the final report).
|
|
71
|
+
|
|
72
|
+
## Step 3 — Generate secrets and set dev-time config
|
|
73
|
+
|
|
74
|
+
The project already has `config/local.yaml` (seeded by the CLI from `config/_local.yaml`). Fill in
|
|
75
|
+
dev-time secrets and lenient defaults in place — existing values you didn't touch are preserved:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
node ${CLAUDE_SKILL_DIR}/scripts/gen-secrets.js "$(pwd)" \
|
|
79
|
+
--openai-key "<apiKey>" \
|
|
80
|
+
--openai-base-url "<baseURL>"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This writes into `config/local.yaml`:
|
|
84
|
+
|
|
85
|
+
- `webServer.auth.jwtToken.encryptKey` — fresh UUIDv4
|
|
86
|
+
- `webServer.auth.permanentServerTokens` — `[<32-char hex>]`
|
|
87
|
+
- `agentTester.openAi.apiKey` / `.baseURL` — when provided
|
|
88
|
+
- Lenient dev defaults: `agentTester.{enabled:true, showFooterLink:true, useAuth:false}`,
|
|
89
|
+
`consul.service.enable:false`, `webServer.auth.enabled:false`, `adminPanel.enabled:false`.
|
|
90
|
+
|
|
91
|
+
Report the wrote-keys list back to the user (NOT the actual secret values). If the developer has
|
|
92
|
+
hand-tuned dev flags they don't want clobbered, re-run with `--skip-lenient`.
|
|
93
|
+
|
|
94
|
+
## Step 4 — Install deps & initial build
|
|
95
|
+
|
|
96
|
+
From the project root:
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
yarn install
|
|
100
|
+
yarn cb # clean build
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
If `cb` fails, fix compilation errors before continuing — the rest of the skill depends on a
|
|
104
|
+
working build.
|
|
105
|
+
|
|
106
|
+
## Step 5 — Clean branch, initial commit, create GitLab repo, first push
|
|
107
|
+
|
|
108
|
+
Before planning the feature, land the scaffolded + configured project on GitLab so the rest of
|
|
109
|
+
the work is tracked on the remote. The final push in Step 10 reuses whatever remote is wired up
|
|
110
|
+
here.
|
|
111
|
+
|
|
112
|
+
This step has two branches at the "remote" stage:
|
|
113
|
+
|
|
114
|
+
- **Create new repo** (default) — no pre-existing remote, user didn't veto creation.
|
|
115
|
+
- **Skip creation, push to existing remote** — triggered when the accompanying text explicitly
|
|
116
|
+
says so ("don't create repo", "не создавай репозиторий", "remote already exists", "push to
|
|
117
|
+
`<url>`", "репозиторий уже есть", "origin уже настроен" etc.), OR `git remote -v` already shows
|
|
118
|
+
an `origin` pointing at GitLab. When in doubt, ASK the user before creating — it's cheap to
|
|
119
|
+
confirm, expensive to recover from an accidental duplicate project.
|
|
120
|
+
|
|
121
|
+
**1. Inspect the working tree.** Run `git status` and report the state to the user in plain prose:
|
|
122
|
+
which files are new (untracked), which are modified, which are staged. The user needs to see this
|
|
123
|
+
before anything is committed.
|
|
124
|
+
|
|
125
|
+
**2. Branch must be clean — stash anything that shouldn't enter the initial commit.** "Clean"
|
|
126
|
+
means there are no untracked files and no unstaged modifications left over after you've decided
|
|
127
|
+
what belongs in the initial commit. If the tree contains scratch notes, local-only tweaks, or
|
|
128
|
+
anything the user flagged as not-for-commit, stash it with an untracked-inclusive stash:
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
git stash push -u -m "deploy-mcp: pre-initial-push stash" -- <paths>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Announce what you stashed so the user can recover it later via `git stash list` / `git stash pop`.
|
|
135
|
+
Re-run `git status` to confirm the tree now contains only files that belong in the scaffold commit.
|
|
136
|
+
|
|
137
|
+
**3. Commit the scaffolded state.** Stage everything that should be on the remote and commit with
|
|
138
|
+
a clear message:
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
git add -A
|
|
142
|
+
git commit -m "chore: initial scaffold (fa-mcp)"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
If `git status` was already clean with a prior commit present, skip this — there is nothing new
|
|
146
|
+
to commit.
|
|
147
|
+
|
|
148
|
+
**4. Decide the branch.** Run `git remote -v` and compare against the accompanying text:
|
|
149
|
+
|
|
150
|
+
- If the text says "don't create" / "repo already exists" / names an explicit remote URL, OR
|
|
151
|
+
`git remote -v` already shows an `origin` → go to **4a (skip creation)**.
|
|
152
|
+
- If neither signal is present, confirm creation with the user in one short question
|
|
153
|
+
(e.g. *"Создать новый репозиторий в GitLab или использовать существующий? Если существующий — дай
|
|
154
|
+
URL."*), then branch accordingly.
|
|
155
|
+
|
|
156
|
+
### 4a. Skip creation — push to existing remote
|
|
157
|
+
|
|
158
|
+
No GitLab API call; no `gitlab-push.js`. Just wire `origin` to the existing URL and push:
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
# If origin isn't set yet, add it. If it's set to the wrong URL, update it.
|
|
162
|
+
git remote add origin <ssh-or-https-url> # first time
|
|
163
|
+
# or
|
|
164
|
+
git remote set-url origin <ssh-or-https-url> # replacing
|
|
165
|
+
|
|
166
|
+
git checkout -B main
|
|
167
|
+
git push -u origin main
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Record the remote URL for Step 10. You do NOT need `baseUrl`, `token`, or `group` in this branch —
|
|
171
|
+
authentication happens via the user's existing SSH key / git credential helper. If the push fails
|
|
172
|
+
with an auth error, surface it to the user; do not attempt API-token workarounds.
|
|
173
|
+
|
|
174
|
+
### 4b. Create new repo via gitlab-push.js
|
|
175
|
+
|
|
176
|
+
Collect GitLab credentials — prefer values already in the accompanying text, ask only for what's
|
|
177
|
+
missing:
|
|
178
|
+
|
|
179
|
+
- `baseUrl` — e.g. `https://gitlab.finam.ru/api/v4`
|
|
180
|
+
- `token` — GitLab private token with `api` scope
|
|
181
|
+
- `group` — group name or full path (e.g. `mcp-servers` or `ai/mcp`), OR `groupId` numeric
|
|
182
|
+
|
|
183
|
+
If the user gives a group **name**, the push script resolves it to `groupId` via
|
|
184
|
+
`GET /groups?search=<name>`.
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
node ${CLAUDE_SKILL_DIR}/scripts/gitlab-push.js \
|
|
188
|
+
--base-url "<baseUrl>" \
|
|
189
|
+
--token "<token>" \
|
|
190
|
+
--group "<group>" \
|
|
191
|
+
--name "<project.name>" \
|
|
192
|
+
--cwd "$(pwd)"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The script: resolves `groupId` → `POST /projects` with `{ name, path, namespace_id, visibility: private }`
|
|
196
|
+
→ `git init` (if needed) → `git checkout -B main` → `git add -A` → commit (if anything to commit)
|
|
197
|
+
→ `git remote add origin <ssh_url>` → `git push -u origin main`.
|
|
198
|
+
|
|
199
|
+
If creation or push fails, surface the HTTP body / git stderr to the user — do NOT retry silently.
|
|
200
|
+
A common failure is "path has already been taken" — ask the user for a different `--path` (URL slug),
|
|
201
|
+
OR switch to branch 4a if the "collision" is in fact the already-existing target repo.
|
|
202
|
+
|
|
203
|
+
**5. Remember the remote URL for Step 10.** Step 10 does NOT re-create the project — only
|
|
204
|
+
`git push` against the same remote, regardless of which branch (4a or 4b) you took here.
|
|
205
|
+
|
|
206
|
+
## Step 6 — Draft and commit to a plan
|
|
207
|
+
|
|
208
|
+
Create `claudedocs/impl-plan.md` (create the directory if needed). Structure:
|
|
209
|
+
|
|
210
|
+
```markdown
|
|
211
|
+
# Implementation Plan — <project name>
|
|
212
|
+
|
|
213
|
+
## Goal
|
|
214
|
+
<One paragraph restating the feature from the accompanying text.>
|
|
215
|
+
|
|
216
|
+
## Tools
|
|
217
|
+
- [ ] `<tool_name>` — <description>; params: …; expected result: …
|
|
218
|
+
- [ ] …
|
|
219
|
+
|
|
220
|
+
## Resources
|
|
221
|
+
- [ ] `<resource_uri>` — …
|
|
222
|
+
|
|
223
|
+
## Prompts
|
|
224
|
+
- [ ] `AGENT_BRIEF` — …
|
|
225
|
+
- [ ] `AGENT_PROMPT` — …
|
|
226
|
+
|
|
227
|
+
## REST endpoints (if any)
|
|
228
|
+
- [ ] `GET /api/<…>` — …
|
|
229
|
+
|
|
230
|
+
## Configuration additions to default.yaml
|
|
231
|
+
- [ ] `accessPoints.<name>` / `db.postgres.dbs.<name>` / etc.
|
|
232
|
+
|
|
233
|
+
## Test cases (tests/mcp/test-cases.js)
|
|
234
|
+
- [ ] happy path per tool
|
|
235
|
+
- [ ] invalid params / missing required
|
|
236
|
+
- [ ] upstream errors
|
|
237
|
+
|
|
238
|
+
## Agent Tester scenarios
|
|
239
|
+
- [ ] <user-question-1> → expects <tool>/<behaviour>
|
|
240
|
+
- [ ] …
|
|
241
|
+
|
|
242
|
+
## Sign-off
|
|
243
|
+
- [ ] `yarn cb` clean
|
|
244
|
+
- [ ] `yarn lint:fix` clean
|
|
245
|
+
- [ ] `yarn typecheck` clean
|
|
246
|
+
- [ ] `yarn test:mcp`, `:mcp-http`, `:mcp-sse` all green
|
|
247
|
+
- [ ] Agent Tester iterations done, `claudedocs/test-log.md` has entries
|
|
248
|
+
- [ ] `claudedocs/dev-report.md` written
|
|
249
|
+
- [ ] Final GitLab push (Step 10) complete
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Tick boxes as you go. The plan is not optional — it is how the user audits progress.
|
|
253
|
+
|
|
254
|
+
## Step 7 — Implement
|
|
255
|
+
|
|
256
|
+
Follow the plan. For each tool/resource/prompt:
|
|
257
|
+
|
|
258
|
+
1. Edit `src/tools/tools.ts`, `src/tools/handle-tool-call.ts`, `src/custom-resources.ts`,
|
|
259
|
+
`src/api/router.ts`, `src/prompts/*` as needed. Replace the stub `example_tool` — do not
|
|
260
|
+
leave demo code in the final build.
|
|
261
|
+
2. Add new config keys to `config/default.yaml` (and matching env mappings in
|
|
262
|
+
`config/custom-environment-variables.yaml` when appropriate). Mirror structural changes
|
|
263
|
+
in `config/_local.yaml`.
|
|
264
|
+
3. Update `tests/mcp/test-cases.js` with real cases.
|
|
265
|
+
4. `yarn cb` after each meaningful change; don't accumulate type errors.
|
|
266
|
+
|
|
267
|
+
Reference docs live in `FA-MCP-SDK-DOC/` — read them if you are unsure about an API
|
|
268
|
+
(`01-getting-started.md`, `02-1-tools-and-api.md`, `02-2-prompts-and-resources.md`,
|
|
269
|
+
`03-configuration.md`, `08-agent-tester-and-headless-api.md`).
|
|
270
|
+
|
|
271
|
+
## Step 8 — Headless Agent Tester loop
|
|
272
|
+
|
|
273
|
+
The key was already verified against the endpoint in Step 2. Here the remaining concern is that
|
|
274
|
+
`config/local.yaml` was written correctly and the project can actually load the key at runtime.
|
|
275
|
+
Run the project's own `check-llm` as a config-path sanity gate:
|
|
276
|
+
|
|
277
|
+
```
|
|
278
|
+
yarn check-llm
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Non-zero exit at this point almost always means the key wasn't persisted into `config/local.yaml`
|
|
282
|
+
(or the project reads a different path than expected) — NOT that the key itself is invalid. Diagnose
|
|
283
|
+
by checking `config/local.yaml` for `agentTester.openAi.apiKey` before asking the user for a new key.
|
|
284
|
+
|
|
285
|
+
Start the server (background):
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
yarn start &
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Check it came up:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
curl -sS http://localhost:<port>/agent-tester/api/mcp/status
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
(`<port>` comes from `config/default.yaml` → `webServer.port`.) Verify the expected tools are listed.
|
|
298
|
+
|
|
299
|
+
Then iterate. For an **independent** scenario (one-shot question, no prior context):
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
node ${CLAUDE_SKILL_DIR}/scripts/headless-test.js \
|
|
303
|
+
--port <port> \
|
|
304
|
+
--message "<user question>" \
|
|
305
|
+
--verbose
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
For a **multi-turn** scenario (follow-up question refers back to earlier context), pin a session
|
|
309
|
+
so the server-side dialog history is preserved across calls:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
# First question — session file is created and sessionId is written into it.
|
|
313
|
+
node ${CLAUDE_SKILL_DIR}/scripts/headless-test.js \
|
|
314
|
+
--port <port> \
|
|
315
|
+
--session-file claudedocs/.agent-session \
|
|
316
|
+
--message "<first question>" --verbose
|
|
317
|
+
|
|
318
|
+
# Follow-up — reuses the same sessionId from the file automatically.
|
|
319
|
+
node ${CLAUDE_SKILL_DIR}/scripts/headless-test.js \
|
|
320
|
+
--port <port> \
|
|
321
|
+
--session-file claudedocs/.agent-session \
|
|
322
|
+
--message "<follow-up question>" --verbose
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Delete `claudedocs/.agent-session` between unrelated scenario groups to avoid context bleed.
|
|
326
|
+
|
|
327
|
+
For a prepared sequence of turns, use the batch wrapper — one text file, one user message per
|
|
328
|
+
non-empty line (comments start with `#`):
|
|
329
|
+
|
|
330
|
+
```
|
|
331
|
+
node ${CLAUDE_SKILL_DIR}/scripts/headless-chat.js \
|
|
332
|
+
--port <port> \
|
|
333
|
+
--messages claudedocs/scenarios/<name>.txt \
|
|
334
|
+
--session-file claudedocs/.agent-session \
|
|
335
|
+
--out claudedocs/scenarios/<name>.out.json \
|
|
336
|
+
--verbose
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
Parse the JSON response(s). For each turn check:
|
|
340
|
+
|
|
341
|
+
- `trace.tools_used` — the agent called the expected tool?
|
|
342
|
+
- `trace.turns[].tool_calls[].arguments` — args match what the question implies?
|
|
343
|
+
- `trace.turns[].tool_results[].result` — handler returned sensible data?
|
|
344
|
+
- `message` — final reply is accurate and useful?
|
|
345
|
+
- `trace.system_prompt_sent` — the prompt actually sent (useful when iterating on `AGENT_PROMPT`).
|
|
346
|
+
|
|
347
|
+
When something is off, diagnose the root cause (one of: tool description, parameter schema,
|
|
348
|
+
agent prompt, handler logic, error message — per `FA-MCP-SDK-DOC/08-agent-tester-and-headless-api.md`),
|
|
349
|
+
fix, rebuild (`yarn cb`), restart, and re-run the scenario. After restart, in-memory sessions on
|
|
350
|
+
the server are wiped — delete the stale `claudedocs/.agent-session` file before re-running.
|
|
351
|
+
|
|
352
|
+
Log every iteration in `claudedocs/test-log.md` (session header + per-scenario: sent / expected /
|
|
353
|
+
received / tools used / result / diagnosis / fix). This is the audit trail.
|
|
354
|
+
|
|
355
|
+
Stop the server with `node scripts/kill-port.js <port>` (or Ctrl+C) when you're done iterating.
|
|
356
|
+
|
|
357
|
+
## Step 9 — Final quality gates
|
|
358
|
+
|
|
359
|
+
All of these must be clean before pushing:
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
yarn lint:fix
|
|
363
|
+
yarn typecheck
|
|
364
|
+
yarn cb
|
|
365
|
+
yarn test:mcp
|
|
366
|
+
yarn test:mcp-http
|
|
367
|
+
yarn test:mcp-sse
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Zero errors, zero warnings that matter, all transport tests green.
|
|
371
|
+
|
|
372
|
+
Write `claudedocs/dev-report.md` per the structure in `CLAUDE.md` → "Development Report"
|
|
373
|
+
(what was built, architecture decisions, agent prompt rationale, test coverage, Agent Tester findings,
|
|
374
|
+
configuration, known limitations).
|
|
375
|
+
|
|
376
|
+
## Step 10 — Final GitLab push
|
|
377
|
+
|
|
378
|
+
The remote was created in Step 5 — do NOT re-run `gitlab-push.js` here. This step commits the
|
|
379
|
+
implemented feature and pushes it on top of the scaffold commit.
|
|
380
|
+
|
|
381
|
+
**1. Branch-clean check, same rule as Step 5.** Run `git status`. If there's scratch / local-only
|
|
382
|
+
content that shouldn't ship to the remote, stash it first:
|
|
383
|
+
|
|
384
|
+
```
|
|
385
|
+
git stash push -u -m "deploy-mcp: pre-final-push stash" -- <paths>
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
Leave anything stashed from Step 5 still stashed — if it shouldn't be in the initial commit, it
|
|
389
|
+
shouldn't be in this one either.
|
|
390
|
+
|
|
391
|
+
**2. Stage and commit** the implemented changes with a message that reflects what was built
|
|
392
|
+
(tools added, endpoints wired, etc. — not just "update"):
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
git add -A
|
|
396
|
+
git commit -m "<feat/fix-scoped message describing the implemented feature>"
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
If `git status` is already clean (nothing to commit after the stash), skip the commit and go
|
|
400
|
+
straight to step 3 — this can happen if all the work ended up in files that were already in the
|
|
401
|
+
initial commit and you haven't changed anything since.
|
|
402
|
+
|
|
403
|
+
**3. Push to the remote set up in Step 5:**
|
|
404
|
+
|
|
405
|
+
```
|
|
406
|
+
git push origin main
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
If the push is rejected because of a non-fast-forward (remote ahead) — something diverged unexpectedly.
|
|
410
|
+
Show the user `git log origin/main..HEAD` and `git log HEAD..origin/main` and ask how to proceed.
|
|
411
|
+
Do NOT `git push --force` without explicit user approval.
|
|
412
|
+
|
|
413
|
+
## Final report
|
|
414
|
+
|
|
415
|
+
Tell the user:
|
|
416
|
+
|
|
417
|
+
1. Project absolute path on disk.
|
|
418
|
+
2. GitLab web URL of the repo (created in Step 5) and confirmation that both the scaffold push
|
|
419
|
+
(Step 5) and the feature push (Step 10) landed on `main`.
|
|
420
|
+
3. Summary of tools/resources/prompts/endpoints that were implemented.
|
|
421
|
+
4. Any flagged limitations from the dev report.
|
|
422
|
+
5. Link to `claudedocs/impl-plan.md`, `claudedocs/test-log.md`, `claudedocs/dev-report.md`.
|
|
423
|
+
6. Anything still stashed from Step 5 / Step 10 (so the user remembers to `git stash pop` or drop).
|
|
424
|
+
|
|
425
|
+
## Troubleshooting
|
|
426
|
+
|
|
427
|
+
**`yarn check-llm` exits non-zero with a config error** — the OpenAI key wasn't persisted into
|
|
428
|
+
`config/local.yaml`. Re-run Step 3 (`gen-secrets.js`) and verify the file before re-trying.
|
|
429
|
+
|
|
430
|
+
**Agent Tester returns 404 on `/agent-tester/*`** — `agentTester.enabled` is false. `gen-secrets.js`
|
|
431
|
+
sets it true; if still 404, rebuild (`yarn cb`) and verify `config/local.yaml` after the run.
|
|
432
|
+
|
|
433
|
+
**Headless test returns `modelConfig` errors** — the OpenAI key is wrong / out of credits / the model
|
|
434
|
+
name doesn't exist on the configured `baseURL`. Run `yarn check-llm` (optionally with a specific
|
|
435
|
+
model name) to isolate.
|
|
436
|
+
|
|
437
|
+
**GitLab push fails with 401** — token lacks `api` scope or expired. Ask for a fresh token.
|
|
438
|
+
|
|
439
|
+
**GitLab push fails with "path has already been taken"** — slug collision. Ask the user for a
|
|
440
|
+
different `--path` value (the URL slug, separate from `--name`).
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Early-stage sanity check for an OpenAI-compatible LLM endpoint.
|
|
4
|
+
*
|
|
5
|
+
* Runs BEFORE fa-mcp scaffolds the project — so we can't rely on the project's
|
|
6
|
+
* `npm run check-llm` yet. This script calls GET /v1/models against the given
|
|
7
|
+
* baseURL (or https://api.openai.com/v1 by default) with the provided key.
|
|
8
|
+
*
|
|
9
|
+
* Exit codes:
|
|
10
|
+
* 0 — HTTP 2xx received (key looks valid for this endpoint)
|
|
11
|
+
* 1 — HTTP 401/403 (key missing/invalid/insufficient permissions)
|
|
12
|
+
* 2 — transport error (DNS, TCP, TLS, timeout)
|
|
13
|
+
* 3 — unexpected HTTP status (4xx/5xx other than 401/403)
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* node check-openai.js --key <apiKey> [--base-url <url>] [--timeout 15000]
|
|
17
|
+
*
|
|
18
|
+
* Examples:
|
|
19
|
+
* node check-openai.js --key sk-XXXX
|
|
20
|
+
* node check-openai.js --key sk-XXXX --base-url https://my-proxy.example/v1
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import https from 'https';
|
|
24
|
+
import http from 'http';
|
|
25
|
+
import { URL } from 'url';
|
|
26
|
+
|
|
27
|
+
function getOpt (flag, fallback) {
|
|
28
|
+
const i = process.argv.indexOf(flag);
|
|
29
|
+
return i >= 0 ? process.argv[i + 1] : fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const key = getOpt('--key');
|
|
33
|
+
const baseUrl = (getOpt('--base-url') || 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
34
|
+
const timeout = Number(getOpt('--timeout', '15000'));
|
|
35
|
+
|
|
36
|
+
if (!key) {
|
|
37
|
+
console.error('ERROR: --key is required.');
|
|
38
|
+
console.error('Usage: check-openai.js --key <apiKey> [--base-url <url>]');
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const url = `${baseUrl}/models`;
|
|
43
|
+
const u = new URL(url);
|
|
44
|
+
const lib = u.protocol === 'http:' ? http : https;
|
|
45
|
+
|
|
46
|
+
const req = lib.request({
|
|
47
|
+
method: 'GET',
|
|
48
|
+
hostname: u.hostname,
|
|
49
|
+
port: u.port || (u.protocol === 'http:' ? 80 : 443),
|
|
50
|
+
path: u.pathname + u.search,
|
|
51
|
+
headers: {
|
|
52
|
+
'Authorization': `Bearer ${key}`,
|
|
53
|
+
'Accept': 'application/json',
|
|
54
|
+
},
|
|
55
|
+
timeout,
|
|
56
|
+
}, (res) => {
|
|
57
|
+
const chunks = [];
|
|
58
|
+
res.on('data', (c) => chunks.push(c));
|
|
59
|
+
res.on('end', () => {
|
|
60
|
+
const body = Buffer.concat(chunks).toString('utf8');
|
|
61
|
+
const status = res.statusCode || 0;
|
|
62
|
+
if (status >= 200 && status < 300) {
|
|
63
|
+
console.log(`OK (${status}) — ${url}`);
|
|
64
|
+
process.exit(0);
|
|
65
|
+
}
|
|
66
|
+
if (status === 401 || status === 403) {
|
|
67
|
+
console.error(`FAIL (${status}): key rejected by ${url}`);
|
|
68
|
+
console.error(body.slice(0, 500));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
console.error(`FAIL (${status}): unexpected response from ${url}`);
|
|
72
|
+
console.error(body.slice(0, 500));
|
|
73
|
+
process.exit(3);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
req.on('error', (e) => { console.error(`TRANSPORT ERROR: ${e.message}`); process.exit(2); });
|
|
78
|
+
req.on('timeout', () => { req.destroy(new Error(`timeout after ${timeout}ms`)); });
|
|
79
|
+
req.end();
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate and inject dev-mode secrets + lenient defaults into a generated
|
|
4
|
+
* MCP project's config/local.yaml (the file that the CLI derived from
|
|
5
|
+
* config/_local.yaml and which overrides default.yaml locally).
|
|
6
|
+
*
|
|
7
|
+
* What this script writes:
|
|
8
|
+
* webServer.auth.jwtToken.encryptKey — random UUIDv4
|
|
9
|
+
* webServer.auth.permanentServerTokens — [<random 32-char hex>]
|
|
10
|
+
*
|
|
11
|
+
* What it writes ONLY when the corresponding CLI flag is provided:
|
|
12
|
+
* agentTester.openAi.apiKey — --openai-key <key>
|
|
13
|
+
* agentTester.openAi.baseURL — --openai-base-url <url>
|
|
14
|
+
*
|
|
15
|
+
* Lenient dev-time overrides (always written, for easy local testing):
|
|
16
|
+
* agentTester.enabled: true
|
|
17
|
+
* agentTester.showFooterLink: true
|
|
18
|
+
* agentTester.useAuth: false
|
|
19
|
+
* consul.service.enable: false
|
|
20
|
+
* webServer.auth.enabled: false
|
|
21
|
+
* adminPanel.enabled: false
|
|
22
|
+
*
|
|
23
|
+
* Existing values in local.yaml are preserved unless explicitly overridden
|
|
24
|
+
* by the rules above. This uses a minimal deep-merge that only overrides
|
|
25
|
+
* the keys listed; unrelated keys the developer already set are kept.
|
|
26
|
+
*
|
|
27
|
+
* Usage:
|
|
28
|
+
* node gen-secrets.js <project-root>
|
|
29
|
+
* [--openai-key <key>] [--openai-base-url <url>]
|
|
30
|
+
* [--skip-lenient] # don't write the lenient dev overrides
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import fs from 'fs';
|
|
34
|
+
import path from 'path';
|
|
35
|
+
import crypto from 'crypto';
|
|
36
|
+
import yaml from 'js-yaml';
|
|
37
|
+
|
|
38
|
+
function getOpt (flag) {
|
|
39
|
+
const i = process.argv.indexOf(flag);
|
|
40
|
+
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
41
|
+
}
|
|
42
|
+
function hasFlag (flag) {
|
|
43
|
+
return process.argv.includes(flag);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const projectRoot = process.argv[2];
|
|
47
|
+
if (!projectRoot) {
|
|
48
|
+
console.error('Usage: gen-secrets.js <project-root> [--openai-key K] [--openai-base-url URL] [--skip-lenient]');
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const openaiKey = getOpt('--openai-key');
|
|
53
|
+
const openaiBaseUrl = getOpt('--openai-base-url');
|
|
54
|
+
const skipLenient = hasFlag('--skip-lenient');
|
|
55
|
+
|
|
56
|
+
const localPath = path.resolve(projectRoot, 'config', 'local.yaml');
|
|
57
|
+
if (!fs.existsSync(localPath)) {
|
|
58
|
+
console.error(`Not found: ${localPath}. Run fa-mcp first.`);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let local = {};
|
|
63
|
+
const raw = fs.readFileSync(localPath, 'utf8');
|
|
64
|
+
if (raw.trim()) {
|
|
65
|
+
local = yaml.load(raw, { schema: yaml.DEFAULT_SCHEMA }) || {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function set (root, pathArr, value) {
|
|
69
|
+
let o = root;
|
|
70
|
+
for (let i = 0; i < pathArr.length - 1; i++) {
|
|
71
|
+
const k = pathArr[i];
|
|
72
|
+
if (typeof o[k] !== 'object' || o[k] === null) o[k] = {};
|
|
73
|
+
o = o[k];
|
|
74
|
+
}
|
|
75
|
+
o[pathArr[pathArr.length - 1]] = value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const encryptKey = crypto.randomUUID();
|
|
79
|
+
const permToken = crypto.randomBytes(16).toString('hex');
|
|
80
|
+
|
|
81
|
+
set(local, ['webServer', 'auth', 'jwtToken', 'encryptKey'], encryptKey);
|
|
82
|
+
set(local, ['webServer', 'auth', 'permanentServerTokens'], [permToken]);
|
|
83
|
+
|
|
84
|
+
if (openaiKey) set(local, ['agentTester', 'openAi', 'apiKey'], openaiKey);
|
|
85
|
+
if (openaiBaseUrl) set(local, ['agentTester', 'openAi', 'baseURL'], openaiBaseUrl);
|
|
86
|
+
|
|
87
|
+
if (!skipLenient) {
|
|
88
|
+
set(local, ['agentTester', 'enabled'], true);
|
|
89
|
+
set(local, ['agentTester', 'showFooterLink'], true);
|
|
90
|
+
set(local, ['agentTester', 'useAuth'], false);
|
|
91
|
+
set(local, ['consul', 'service', 'enable'], false);
|
|
92
|
+
set(local, ['webServer', 'auth', 'enabled'], false);
|
|
93
|
+
set(local, ['adminPanel', 'enabled'], false);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const out = yaml.dump(local, { lineWidth: 120, quotingType: '"' });
|
|
97
|
+
fs.writeFileSync(localPath, out, 'utf8');
|
|
98
|
+
|
|
99
|
+
const wrote = [
|
|
100
|
+
'webServer.auth.jwtToken.encryptKey',
|
|
101
|
+
'webServer.auth.permanentServerTokens',
|
|
102
|
+
];
|
|
103
|
+
if (openaiKey) wrote.push('agentTester.openAi.apiKey');
|
|
104
|
+
if (openaiBaseUrl) wrote.push('agentTester.openAi.baseURL');
|
|
105
|
+
if (!skipLenient) {
|
|
106
|
+
wrote.push('agentTester.{enabled,showFooterLink,useAuth}', 'consul.service.enable',
|
|
107
|
+
'webServer.auth.enabled', 'adminPanel.enabled');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const report = {
|
|
111
|
+
path: localPath,
|
|
112
|
+
encryptKey,
|
|
113
|
+
permanentServerToken: permToken,
|
|
114
|
+
wroteKeys: wrote,
|
|
115
|
+
};
|
|
116
|
+
process.stdout.write(JSON.stringify(report, null, 2));
|