fa-mcp-sdk 0.4.61 → 0.4.65
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/package.json +1 -1
- package/cli-template/readme-docs/SKILLS.md +49 -0
- package/cli-template/tsconfig.json +8 -1
- package/package.json +3 -2
- package/cli-template/deploy/.gitkeep +0 -0
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));
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Create a project on a GitLab server and push the current directory to it.
|
|
4
|
+
*
|
|
5
|
+
* Resolves groupId from --group <name> via GET /groups?search=<name> (exact match on
|
|
6
|
+
* `full_path` or `name`), then POSTs /projects with { name, path, namespace_id }.
|
|
7
|
+
* Finally runs `git init / add / commit / remote add / push -u origin <branch>`.
|
|
8
|
+
*
|
|
9
|
+
* Environment / flags:
|
|
10
|
+
* --base-url <url> (e.g. https://gitlab.finam.ru/api/v4) — required
|
|
11
|
+
* --token <tok> GitLab private token — required
|
|
12
|
+
* --group <name> Group name or full path (e.g. "mcp-servers") — required unless --group-id is given
|
|
13
|
+
* --group-id <n> Numeric group id — overrides --group lookup
|
|
14
|
+
* --name <name> Project name — required
|
|
15
|
+
* --path <slug> URL slug (kebab-case). Defaults to --name lowercased / slugified
|
|
16
|
+
* --cwd <dir> Directory to push. Defaults to process.cwd()
|
|
17
|
+
* --branch <name> Branch to push. Defaults to "main"
|
|
18
|
+
* --visibility <v> private|internal|public (default: private)
|
|
19
|
+
* --dry-run Print what would happen, don't call API or git
|
|
20
|
+
*
|
|
21
|
+
* ENV fallbacks: GITLAB_BASE_URL, GITLAB_TOKEN, GITLAB_GROUP, GITLAB_GROUP_ID.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import https from 'https';
|
|
25
|
+
import http from 'http';
|
|
26
|
+
import { URL } from 'url';
|
|
27
|
+
import { execSync } from 'child_process';
|
|
28
|
+
import fs from 'fs';
|
|
29
|
+
import path from 'path';
|
|
30
|
+
|
|
31
|
+
function getOpt (flag) {
|
|
32
|
+
const i = process.argv.indexOf(flag);
|
|
33
|
+
return i >= 0 ? process.argv[i + 1] : undefined;
|
|
34
|
+
}
|
|
35
|
+
function hasFlag (flag) {
|
|
36
|
+
return process.argv.includes(flag);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const baseUrl = getOpt('--base-url') || process.env.GITLAB_BASE_URL;
|
|
40
|
+
const token = getOpt('--token') || process.env.GITLAB_TOKEN;
|
|
41
|
+
let groupArg = getOpt('--group') || process.env.GITLAB_GROUP;
|
|
42
|
+
let groupId = getOpt('--group-id') || process.env.GITLAB_GROUP_ID;
|
|
43
|
+
const projectName = getOpt('--name');
|
|
44
|
+
const projectPath = getOpt('--path') || (projectName
|
|
45
|
+
? projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
|
46
|
+
: undefined);
|
|
47
|
+
const cwd = path.resolve(getOpt('--cwd') || process.cwd());
|
|
48
|
+
const branch = getOpt('--branch') || 'main';
|
|
49
|
+
const visibility = getOpt('--visibility') || 'private';
|
|
50
|
+
const dryRun = hasFlag('--dry-run');
|
|
51
|
+
|
|
52
|
+
function die (msg, code = 1) {
|
|
53
|
+
console.error(`ERROR: ${msg}`);
|
|
54
|
+
process.exit(code);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!baseUrl) die('Missing --base-url (or GITLAB_BASE_URL). Example: https://gitlab.finam.ru/api/v4');
|
|
58
|
+
if (!token) die('Missing --token (or GITLAB_TOKEN).');
|
|
59
|
+
if (!projectName) die('Missing --name (project name).');
|
|
60
|
+
if (!groupId && !groupArg) die('Missing --group or --group-id.');
|
|
61
|
+
|
|
62
|
+
function request (method, url, bodyObj) {
|
|
63
|
+
const u = new URL(url);
|
|
64
|
+
const lib = u.protocol === 'http:' ? http : https;
|
|
65
|
+
const body = bodyObj ? JSON.stringify(bodyObj) : null;
|
|
66
|
+
const opts = {
|
|
67
|
+
method,
|
|
68
|
+
hostname: u.hostname,
|
|
69
|
+
port: u.port || (u.protocol === 'http:' ? 80 : 443),
|
|
70
|
+
path: u.pathname + u.search,
|
|
71
|
+
headers: {
|
|
72
|
+
'PRIVATE-TOKEN': token,
|
|
73
|
+
'Accept': 'application/json',
|
|
74
|
+
...(body ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) } : {}),
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
return new Promise((resolve, reject) => {
|
|
78
|
+
const req = lib.request(opts, (res) => {
|
|
79
|
+
const chunks = [];
|
|
80
|
+
res.on('data', (c) => chunks.push(c));
|
|
81
|
+
res.on('end', () => {
|
|
82
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
83
|
+
let json;
|
|
84
|
+
try { json = text ? JSON.parse(text) : {}; } catch { json = { raw: text }; }
|
|
85
|
+
if (res.statusCode >= 200 && res.statusCode < 300) resolve(json);
|
|
86
|
+
else reject(new Error(`HTTP ${res.statusCode} ${u.pathname}: ${text}`));
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
req.on('error', reject);
|
|
90
|
+
if (body) req.write(body);
|
|
91
|
+
req.end();
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function sh (cmd, opts = {}) {
|
|
96
|
+
console.log(` $ ${cmd}`);
|
|
97
|
+
if (dryRun) return '';
|
|
98
|
+
return execSync(cmd, { stdio: ['ignore', 'pipe', 'pipe'], cwd, ...opts }).toString().trim();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function resolveGroupId () {
|
|
102
|
+
if (groupId) return Number(groupId);
|
|
103
|
+
console.log(`[gitlab] Looking up group "${groupArg}" …`);
|
|
104
|
+
const url = `${baseUrl.replace(/\/$/, '')}/groups?search=${encodeURIComponent(groupArg)}&per_page=100`;
|
|
105
|
+
if (dryRun) return 0;
|
|
106
|
+
const res = await request('GET', url);
|
|
107
|
+
if (!Array.isArray(res) || res.length === 0) die(`No groups found matching "${groupArg}".`);
|
|
108
|
+
const exact = res.find((g) => g.full_path === groupArg || g.path === groupArg || g.name === groupArg);
|
|
109
|
+
const pick = exact || res[0];
|
|
110
|
+
console.log(`[gitlab] group: ${pick.full_path} (id=${pick.id})`);
|
|
111
|
+
return pick.id;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function createProject (namespaceId) {
|
|
115
|
+
console.log(`[gitlab] Creating project "${projectName}" (path=${projectPath}) in namespace ${namespaceId} …`);
|
|
116
|
+
const url = `${baseUrl.replace(/\/$/, '')}/projects`;
|
|
117
|
+
if (dryRun) return { ssh_url_to_repo: 'git@example:stub.git', http_url_to_repo: 'https://example/stub.git', web_url: 'https://example/stub' };
|
|
118
|
+
const body = {
|
|
119
|
+
name: projectName,
|
|
120
|
+
path: projectPath,
|
|
121
|
+
namespace_id: namespaceId,
|
|
122
|
+
visibility,
|
|
123
|
+
initialize_with_readme: false,
|
|
124
|
+
};
|
|
125
|
+
const res = await request('POST', url, body);
|
|
126
|
+
console.log(`[gitlab] created: ${res.web_url}`);
|
|
127
|
+
return res;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function gitPush (remoteUrl) {
|
|
131
|
+
console.log(`[git] Initializing and pushing ${cwd} to ${remoteUrl} …`);
|
|
132
|
+
if (!fs.existsSync(path.join(cwd, '.git'))) sh('git init');
|
|
133
|
+
sh(`git checkout -B ${branch}`);
|
|
134
|
+
sh('git add -A');
|
|
135
|
+
try {
|
|
136
|
+
sh('git diff --cached --quiet');
|
|
137
|
+
console.log('[git] Nothing to commit — working tree already clean.');
|
|
138
|
+
} catch {
|
|
139
|
+
sh('git commit -m "Initial commit (scaffolded by fa-mcp)"');
|
|
140
|
+
}
|
|
141
|
+
try { sh('git remote remove origin'); } catch { /* no origin yet */ }
|
|
142
|
+
sh(`git remote add origin ${remoteUrl}`);
|
|
143
|
+
sh(`git push -u origin ${branch}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
(async () => {
|
|
147
|
+
try {
|
|
148
|
+
const nsId = await resolveGroupId();
|
|
149
|
+
const project = await createProject(nsId);
|
|
150
|
+
const remote = project.ssh_url_to_repo || project.http_url_to_repo;
|
|
151
|
+
if (!remote) die('GitLab did not return a repo URL.');
|
|
152
|
+
gitPush(remote);
|
|
153
|
+
console.log(`\nDone. ${project.web_url || remote}`);
|
|
154
|
+
} catch (e) {
|
|
155
|
+
die(e.message);
|
|
156
|
+
}
|
|
157
|
+
})();
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Multi-turn wrapper around POST /agent-tester/api/chat/test. Sends a sequence of user messages
|
|
4
|
+
* in a single server-side session so the agent retains dialog history across questions.
|
|
5
|
+
*
|
|
6
|
+
* Input: a plain text file, one message per non-empty line. Lines starting with '#' are ignored.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* node headless-chat.js --port 9876 --messages scenarios.txt [options]
|
|
10
|
+
*
|
|
11
|
+
* Options:
|
|
12
|
+
* --port <n> Web server port (required)
|
|
13
|
+
* --messages <path> Text file, one user message per line (required)
|
|
14
|
+
* --auth <header> Full Authorization header value. Optional.
|
|
15
|
+
* --verbose Include per-turn LLM request/response in trace
|
|
16
|
+
* --max-result <n> Max chars per tool result (default 4000)
|
|
17
|
+
* --max-trace <n> Max total trace size (default 50000)
|
|
18
|
+
* --agent-prompt <s> Override system prompt (applied to the whole dialog)
|
|
19
|
+
* --model <name> Model name (default: let server choose)
|
|
20
|
+
* --timeout <ms> Request timeout per message (default 120000)
|
|
21
|
+
* --session <id> Start from an existing sessionId instead of a fresh one
|
|
22
|
+
* --session-file <path> Persist final sessionId (same semantics as headless-test.js)
|
|
23
|
+
* --stop-on-error Abort the sequence on first non-2xx response (default: continue)
|
|
24
|
+
* --out <path> Write an aggregated JSON array of per-turn responses to this file
|
|
25
|
+
*
|
|
26
|
+
* Each response is also printed to stdout as a JSON object prefixed by a header line
|
|
27
|
+
* === MESSAGE <n>/<total>: <first-80-chars> ===
|
|
28
|
+
* Exit code: 0 if all messages returned 2xx, 1 otherwise.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import fs from 'fs';
|
|
32
|
+
import http from 'http';
|
|
33
|
+
|
|
34
|
+
function getOpt (flag, fallback) {
|
|
35
|
+
const i = process.argv.indexOf(flag);
|
|
36
|
+
return i >= 0 ? process.argv[i + 1] : fallback;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasFlag (flag) { return process.argv.includes(flag); }
|
|
40
|
+
|
|
41
|
+
const port = getOpt('--port');
|
|
42
|
+
const messagesArg = getOpt('--messages');
|
|
43
|
+
const auth = getOpt('--auth');
|
|
44
|
+
const verbose = hasFlag('--verbose');
|
|
45
|
+
const maxResult = getOpt('--max-result', '4000');
|
|
46
|
+
const maxTrace = getOpt('--max-trace', '50000');
|
|
47
|
+
const agentPrompt = getOpt('--agent-prompt');
|
|
48
|
+
const model = getOpt('--model');
|
|
49
|
+
const timeout = Number(getOpt('--timeout', '120000'));
|
|
50
|
+
const sessionOpt = getOpt('--session');
|
|
51
|
+
const sessionFile = getOpt('--session-file');
|
|
52
|
+
const stopOnError = hasFlag('--stop-on-error');
|
|
53
|
+
const outPath = getOpt('--out');
|
|
54
|
+
|
|
55
|
+
if (!port || !messagesArg) {
|
|
56
|
+
console.error('Usage: headless-chat.js --port <n> --messages <file> [--session-file <path>] [--verbose]');
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const raw = fs.readFileSync(messagesArg, 'utf8');
|
|
61
|
+
const messages = raw.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#'));
|
|
62
|
+
if (messages.length === 0) {
|
|
63
|
+
console.error(`no messages found in ${messagesArg}`);
|
|
64
|
+
process.exit(2);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let sessionId = sessionOpt;
|
|
68
|
+
if (!sessionId && sessionFile) {
|
|
69
|
+
try {
|
|
70
|
+
const s = fs.readFileSync(sessionFile, 'utf8').trim();
|
|
71
|
+
if (s) sessionId = s;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e.code !== 'ENOENT') {
|
|
74
|
+
console.error(`session-file read error: ${e.message}`);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sendOne (message) {
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
const body = {
|
|
83
|
+
message,
|
|
84
|
+
mcpConfig: { url: `http://localhost:${port}/mcp`, transport: 'http' },
|
|
85
|
+
};
|
|
86
|
+
if (sessionId) body.sessionId = sessionId;
|
|
87
|
+
if (auth) body.mcpConfig.headers = { Authorization: auth };
|
|
88
|
+
if (agentPrompt) body.agentPrompt = agentPrompt;
|
|
89
|
+
if (model) body.modelConfig = { model };
|
|
90
|
+
|
|
91
|
+
const payload = JSON.stringify(body);
|
|
92
|
+
const qs = `?verbose=${verbose}&maxResultChars=${maxResult}&maxTraceChars=${maxTrace}`;
|
|
93
|
+
|
|
94
|
+
const req = http.request({
|
|
95
|
+
hostname: 'localhost',
|
|
96
|
+
port,
|
|
97
|
+
path: `/agent-tester/api/chat/test${qs}`,
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: {
|
|
100
|
+
'Content-Type': 'application/json',
|
|
101
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
102
|
+
...(auth ? { Authorization: auth } : {}),
|
|
103
|
+
},
|
|
104
|
+
timeout,
|
|
105
|
+
}, (res) => {
|
|
106
|
+
const chunks = [];
|
|
107
|
+
res.on('data', (c) => chunks.push(c));
|
|
108
|
+
res.on('end', () => {
|
|
109
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
110
|
+
let parsed = null;
|
|
111
|
+
try { parsed = JSON.parse(text); } catch { /* keep raw */ }
|
|
112
|
+
resolve({ status: res.statusCode, text, parsed });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
req.on('error', reject);
|
|
116
|
+
req.on('timeout', () => { req.destroy(new Error('timeout')); });
|
|
117
|
+
req.write(payload);
|
|
118
|
+
req.end();
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
(async () => {
|
|
123
|
+
const results = [];
|
|
124
|
+
let anyFailure = false;
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < messages.length; i++) {
|
|
127
|
+
const msg = messages[i];
|
|
128
|
+
const header = `=== MESSAGE ${i + 1}/${messages.length}: ${msg.slice(0, 80)} ===`;
|
|
129
|
+
process.stdout.write(header + '\n');
|
|
130
|
+
|
|
131
|
+
let result;
|
|
132
|
+
try {
|
|
133
|
+
result = await sendOne(msg);
|
|
134
|
+
} catch (e) {
|
|
135
|
+
anyFailure = true;
|
|
136
|
+
process.stdout.write(`request error: ${e.message}\n`);
|
|
137
|
+
results.push({ message: msg, error: e.message });
|
|
138
|
+
if (stopOnError) break;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
process.stdout.write(result.text + '\n');
|
|
143
|
+
|
|
144
|
+
if (result.parsed?.sessionId) sessionId = result.parsed.sessionId;
|
|
145
|
+
const ok = result.status >= 200 && result.status < 300;
|
|
146
|
+
if (!ok) anyFailure = true;
|
|
147
|
+
|
|
148
|
+
results.push({
|
|
149
|
+
message: msg,
|
|
150
|
+
status: result.status,
|
|
151
|
+
response: result.parsed ?? result.text,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (!ok && stopOnError) break;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (sessionFile && sessionId) {
|
|
158
|
+
try { fs.writeFileSync(sessionFile, sessionId); } catch (e) { console.error(`session-file write skipped: ${e.message}`); }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (outPath) {
|
|
162
|
+
try { fs.writeFileSync(outPath, JSON.stringify(results, null, 2)); } catch (e) { console.error(`out write failed: ${e.message}`); }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
process.exit(anyFailure ? 1 : 0);
|
|
166
|
+
})();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Thin wrapper around POST /agent-tester/api/chat/test — used by the deploy-mcp skill
|
|
4
|
+
* to exercise the freshly-built MCP server through the full agent loop.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node headless-test.js --port 9876 --message "What is the EUR/USD rate?" [options]
|
|
8
|
+
*
|
|
9
|
+
* Options:
|
|
10
|
+
* --port <n> Web server port (required)
|
|
11
|
+
* --message <text> User message to send (required)
|
|
12
|
+
* --auth <header> Full Authorization header value (e.g. "Bearer xxxx"). Optional.
|
|
13
|
+
* --verbose Include per-turn LLM request/response in trace
|
|
14
|
+
* --max-result <n> Max chars per tool result (default 4000)
|
|
15
|
+
* --max-trace <n> Max total trace size (default 50000)
|
|
16
|
+
* --agent-prompt <s> Override system prompt for this request
|
|
17
|
+
* --model <name> Model name (default: let server choose)
|
|
18
|
+
* --timeout <ms> Request timeout (default 120000)
|
|
19
|
+
* --session <id> Reuse existing server-side dialog history under this sessionId
|
|
20
|
+
* --session-file <path> Persist sessionId in a file: read if exists, write back after response.
|
|
21
|
+
* Chains multiple invocations into one conversation without manual parsing.
|
|
22
|
+
* If both --session and --session-file are given, --session wins for the
|
|
23
|
+
* request; the final sessionId is still written to the file.
|
|
24
|
+
*
|
|
25
|
+
* Prints the JSON response to stdout. Exit code 0 on 2xx, non-zero otherwise.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import fs from 'fs';
|
|
29
|
+
import http from 'http';
|
|
30
|
+
|
|
31
|
+
function getOpt (flag, fallback) {
|
|
32
|
+
const i = process.argv.indexOf(flag);
|
|
33
|
+
return i >= 0 ? process.argv[i + 1] : fallback;
|
|
34
|
+
}
|
|
35
|
+
function hasFlag (flag) { return process.argv.includes(flag); }
|
|
36
|
+
|
|
37
|
+
const port = getOpt('--port');
|
|
38
|
+
const message = getOpt('--message');
|
|
39
|
+
const auth = getOpt('--auth');
|
|
40
|
+
const verbose = hasFlag('--verbose');
|
|
41
|
+
const maxResult = getOpt('--max-result', '4000');
|
|
42
|
+
const maxTrace = getOpt('--max-trace', '50000');
|
|
43
|
+
const agentPrompt = getOpt('--agent-prompt');
|
|
44
|
+
const model = getOpt('--model');
|
|
45
|
+
const timeout = Number(getOpt('--timeout', '120000'));
|
|
46
|
+
const sessionOpt = getOpt('--session');
|
|
47
|
+
const sessionFile = getOpt('--session-file');
|
|
48
|
+
|
|
49
|
+
if (!port || !message) {
|
|
50
|
+
console.error('Usage: headless-test.js --port <n> --message "<text>" [--auth "Bearer ..."] [--verbose] [--model <name>] [--session <id>] [--session-file <path>]');
|
|
51
|
+
process.exit(2);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let sessionId = sessionOpt;
|
|
55
|
+
if (!sessionId && sessionFile) {
|
|
56
|
+
try {
|
|
57
|
+
const raw = fs.readFileSync(sessionFile, 'utf8').trim();
|
|
58
|
+
if (raw) sessionId = raw;
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (e.code !== 'ENOENT') {
|
|
61
|
+
console.error(`session-file read error: ${e.message}`);
|
|
62
|
+
process.exit(2);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const body = {
|
|
68
|
+
message,
|
|
69
|
+
mcpConfig: { url: `http://localhost:${port}/mcp`, transport: 'http' },
|
|
70
|
+
};
|
|
71
|
+
if (sessionId) body.sessionId = sessionId;
|
|
72
|
+
if (auth) body.mcpConfig.headers = { Authorization: auth };
|
|
73
|
+
if (agentPrompt) body.agentPrompt = agentPrompt;
|
|
74
|
+
if (model) body.modelConfig = { model };
|
|
75
|
+
|
|
76
|
+
const qs = `?verbose=${verbose}&maxResultChars=${maxResult}&maxTraceChars=${maxTrace}`;
|
|
77
|
+
const payload = JSON.stringify(body);
|
|
78
|
+
|
|
79
|
+
const req = http.request({
|
|
80
|
+
hostname: 'localhost',
|
|
81
|
+
port,
|
|
82
|
+
path: `/agent-tester/api/chat/test${qs}`,
|
|
83
|
+
method: 'POST',
|
|
84
|
+
headers: {
|
|
85
|
+
'Content-Type': 'application/json',
|
|
86
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
87
|
+
...(auth ? { Authorization: auth } : {}),
|
|
88
|
+
},
|
|
89
|
+
timeout,
|
|
90
|
+
}, (res) => {
|
|
91
|
+
const chunks = [];
|
|
92
|
+
res.on('data', (c) => chunks.push(c));
|
|
93
|
+
res.on('end', () => {
|
|
94
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
95
|
+
process.stdout.write(text);
|
|
96
|
+
if (sessionFile && res.statusCode >= 200 && res.statusCode < 300) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(text);
|
|
99
|
+
if (parsed?.sessionId) fs.writeFileSync(sessionFile, parsed.sessionId);
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error(`session-file write skipped: ${e.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
process.exit(res.statusCode >= 200 && res.statusCode < 300 ? 0 : 1);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
req.on('error', (e) => { console.error(`request error: ${e.message}`); process.exit(1); });
|
|
108
|
+
req.on('timeout', () => { req.destroy(new Error('timeout')); });
|
|
109
|
+
req.write(payload);
|
|
110
|
+
req.end();
|
|
@@ -137,3 +137,52 @@ Characteristics:
|
|
|
137
137
|
/readme-generator refresh the README after adding 3 new tools
|
|
138
138
|
/readme-generator обнови README с учётом того, что теперь подключён PostgreSQL
|
|
139
139
|
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### `/deploy-mcp` — End-to-End MCP Server Implementation
|
|
144
|
+
|
|
145
|
+
Orchestrates the full implementation workflow from feature brief to a live GitLab repo. The project
|
|
146
|
+
must already be scaffolded by the `fa-mcp` CLI — this skill picks up from `yarn install` onwards.
|
|
147
|
+
|
|
148
|
+
Pipeline (10 steps):
|
|
149
|
+
|
|
150
|
+
1. **Requirements scan** — extracts tools, source-of-truth refs, exclusions, and OpenAI creds from
|
|
151
|
+
accompanying messages/files
|
|
152
|
+
2. **OpenAI pre-flight** — `scripts/check-openai.js` validates the key against `GET /v1/models`
|
|
153
|
+
before anything touches `config/local.yaml`
|
|
154
|
+
3. **Dev secrets** — `scripts/gen-secrets.js` writes fresh `jwtToken.encryptKey`,
|
|
155
|
+
`permanentServerTokens`, OpenAI creds, and lenient dev defaults into `config/local.yaml`
|
|
156
|
+
4. **Install & build** — `yarn install` + `yarn cb`
|
|
157
|
+
5. **First GitLab push** — ensures branch is clean (stashing what shouldn't ship), commits the
|
|
158
|
+
scaffold, then either creates a new GitLab repo via `scripts/gitlab-push.js` OR pushes to an
|
|
159
|
+
existing remote when instructed (text says "don't create" / `origin` is already configured)
|
|
160
|
+
6. **Plan** — writes `claudedocs/impl-plan.md` with tools / resources / prompts / REST / config /
|
|
161
|
+
tests / Agent Tester scenarios / sign-off checklist
|
|
162
|
+
7. **Implementation** — edits `src/tools/*`, `src/prompts/*`, `src/custom-resources.ts`,
|
|
163
|
+
`src/api/router.ts`, `config/default.yaml`, `tests/mcp/test-cases.js`; rebuilds after each change
|
|
164
|
+
8. **Agent Tester loop** — `yarn check-llm` → `yarn start` → `scripts/headless-test.js` /
|
|
165
|
+
`scripts/headless-chat.js` against `/agent-tester/api/chat/test`; logs in `claudedocs/test-log.md`
|
|
166
|
+
9. **Quality gates** — `yarn lint:fix`, `yarn typecheck`, `yarn cb`, `yarn test:mcp[-http|-sse]`
|
|
167
|
+
10. **Second GitLab push** — commits implemented feature and `git push origin main` to the remote
|
|
168
|
+
set up in step 5 (no re-creation, never `--force` without explicit approval)
|
|
169
|
+
|
|
170
|
+
Characteristics:
|
|
171
|
+
|
|
172
|
+
- **Launch**: **command-only** via `/deploy-mcp`. `disable-model-invocation: true` — does NOT
|
|
173
|
+
trigger on implicit mentions
|
|
174
|
+
- **Input**: feature brief comes from the accompanying user message(s) and attached files. OpenAI
|
|
175
|
+
and GitLab creds may be supplied inline or asked interactively
|
|
176
|
+
- **Ground rules**: every step explicit and verified; free-form inputs asked in plain prose (never
|
|
177
|
+
predefined options); exclusions from the brief honoured; dev defaults intentionally lenient;
|
|
178
|
+
`.claude/`, `deploy/`, `FA-MCP-SDK-DOC/` are NOT modified unless the brief explicitly says to
|
|
179
|
+
- **Output**: implemented project + `claudedocs/{impl-plan,test-log,dev-report}.md`, GitLab repo
|
|
180
|
+
with two commits on `main` (scaffold + feature)
|
|
181
|
+
|
|
182
|
+
**Examples:**
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
/deploy-mcp
|
|
186
|
+
/deploy-mcp реализуй инструменты из task.md, OpenAI key sk-..., GitLab group mcp-servers
|
|
187
|
+
/deploy-mcp implement tools from the message; repo уже существует, push to git@gitlab.example:ai/mcp-foo.git
|
|
188
|
+
```
|
|
@@ -35,15 +35,22 @@
|
|
|
35
35
|
"exclude": [
|
|
36
36
|
".claude",
|
|
37
37
|
".idea",
|
|
38
|
+
".junie",
|
|
39
|
+
".playwright-mcp",
|
|
38
40
|
".run",
|
|
41
|
+
".serena",
|
|
39
42
|
"_misc",
|
|
40
43
|
"_tmp",
|
|
41
44
|
"config",
|
|
45
|
+
"out",
|
|
42
46
|
"deploy",
|
|
43
47
|
"dist",
|
|
44
48
|
"doc",
|
|
49
|
+
"docs",
|
|
45
50
|
"node_modules",
|
|
46
|
-
"
|
|
51
|
+
"out",
|
|
52
|
+
"coverage",
|
|
53
|
+
"swagger"
|
|
47
54
|
],
|
|
48
55
|
"ts-node": {
|
|
49
56
|
"esm": true
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fa-mcp-sdk",
|
|
3
3
|
"productName": "FA MCP SDK",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.65",
|
|
5
5
|
"description": "Core infrastructure and templates for building Model Context Protocol (MCP) servers with TypeScript",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/core/index.js",
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
"README.md"
|
|
22
22
|
],
|
|
23
23
|
"bin": {
|
|
24
|
-
"fa-mcp": "./bin/fa-mcp.js"
|
|
24
|
+
"fa-mcp": "./bin/fa-mcp.js",
|
|
25
|
+
"fa-mcp-sdk": "./bin/fa-mcp.js"
|
|
25
26
|
},
|
|
26
27
|
"scripts": {
|
|
27
28
|
"start": "npm run template:start",
|
|
File without changes
|