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.
@@ -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();
@@ -354,7 +354,7 @@ Priority: env vars > `config/local.yaml` > `config/{NODE_ENV}.yaml` > `config/de
354
354
 
355
355
  | Key | Description | Default |
356
356
  |---------------------------------------|-----------------------------------------|-----------|
357
- | `mcp.toolAnswerAs` | Response format (`text` / `json`) | `text` |
357
+ | `mcp.tools.answerAs` | Response format (`text` / `json`) | `text` |
358
358
  | `mcp.name` | Name returned to MCP clients | — |
359
359
 
360
360
  ## Upstream `<prefix>`
@@ -339,7 +339,7 @@ Priority: env vars > `config/local.yaml` > `config/{NODE_ENV}.yaml` > `config/de
339
339
  | `<upstream>.auth.basic.password` | Basic auth password | — |
340
340
  | `webServer.port` | HTTP server port | `<PORT>` |
341
341
  | `webServer.auth.enabled` | MCP server authorization on/off | `false` |
342
- | `mcp.toolAnswerAs` | Response format (`text` / `json`) | `text` |
342
+ | `mcp.tools.answerAs` | Response format (`text` / `json`) | `text` |
343
343
 
344
344
  Full reference: [Configuration](./readme-docs/configuration.md).
345
345
  ```
@@ -121,7 +121,7 @@ const dbEnabled = appConfig.isMainDBUsed;
121
121
  | `shortName` | Name without 'mcp' suffix |
122
122
  | `version` | Package version |
123
123
  | `webServer` | HTTP server config (host, port, auth) |
124
- | `mcp` | MCP settings (transportType, rateLimit, tools.hideAnnotations) |
124
+ | `mcp` | MCP settings (transportType, rateLimit, tools.answerAs, tools.hideAnnotations) |
125
125
  | `logger` | Logging config |
126
126
  | `ad` | Active Directory config |
127
127
  | `consul` | Service discovery settings |
@@ -109,11 +109,11 @@ logger:
109
109
 
110
110
  mcp:
111
111
  transportType: http # stdio | http
112
- toolAnswerAs: text # text | structuredContent
113
112
  rateLimit:
114
113
  maxRequests: 100
115
114
  windowMs: 60000
116
115
  tools:
116
+ answerAs: text # text | structuredContent
117
117
  hideAnnotations: false # true — strip `annotations` from tool listings
118
118
 
119
119
  swagger:
@@ -80,21 +80,21 @@ import { getTools, formatToolResult, getJsonFromResult, asTextContent, asJson }
80
80
 
81
81
  const tools = await getTools(); // Get registered tools
82
82
 
83
- // Format based on appConfig.mcp.toolAnswerAs
83
+ // Format based on appConfig.mcp.tools.answerAs
84
84
  const result = formatToolResult({ message: 'Done', data: {} });
85
85
 
86
- // Returns structuredContent or JSON from text depending on appConfig.mcp.toolAnswerAs
86
+ // Returns structuredContent or JSON from text depending on appConfig.mcp.tools.answerAs
87
87
  const original = getJsonFromResult<T>(result);
88
88
 
89
- // Direct formatting helpers (ignore toolAnswerAs config):
89
+ // Direct formatting helpers (ignore tools.answerAs config):
90
90
  asTextContent('Hello'); // { content: [{ type: 'text', text: 'Hello' }] }
91
91
  asJson({ status: 'ok' }); // { structuredContent: { status: 'ok' } }
92
92
  ```
93
93
 
94
94
  ### When to Use Which
95
95
 
96
- - **`formatToolResult()`** — Primary choice in tool handlers. Respects `appConfig.mcp.toolAnswerAs` config.
97
- - **`asTextContent()` / `asJson()`** — Direct formatting, ignores `toolAnswerAs`. Use when specific format needed.
96
+ - **`formatToolResult()`** — Primary choice in tool handlers. Respects `appConfig.mcp.tools.answerAs` config.
97
+ - **`asTextContent()` / `asJson()`** — Direct formatting, ignores `tools.answerAs`. Use when specific format needed.
98
98
  - **`getJsonFromResult()`** — Inverse of `formatToolResult()`. Extracts JSON from either format. Use in tests.
99
99
 
100
100
  ## Network Utilities
@@ -50,7 +50,7 @@
50
50
  "dependencies": {
51
51
  "@modelcontextprotocol/sdk": "^1.29.0",
52
52
  "dotenv": "^17.4.1",
53
- "fa-mcp-sdk": "^0.4.60"
53
+ "fa-mcp-sdk": "^0.4.64"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/express": "^5.0.6",
@@ -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
- "coverage"
51
+ "out",
52
+ "coverage",
53
+ "swagger"
47
54
  ],
48
55
  "ts-node": {
49
56
  "esm": true
@@ -217,18 +217,18 @@ logger:
217
217
  mcp:
218
218
  #> Transport for the MCP server: stdio | http
219
219
  transportType: http
220
- #> Response format configuration.
221
- #> - structuredContent — default — the response in result.structuredContent returns JSON
222
- #> - text — in the response, serialized JSON is returned in result.content[0].text
223
- toolAnswerAs: text
224
220
  #> Per-client request rate limiting for the MCP endpoint
225
221
  rateLimit:
226
222
  #> Maximum number of requests allowed within windowMs
227
223
  maxRequests: 100
228
224
  #> Rate limit window length in milliseconds (1 minute)
229
225
  windowMs: 60000
230
- #> Tool listing behavior
226
+ #> Tool listing and response behavior
231
227
  tools:
228
+ #> Response format configuration.
229
+ #> - structuredContent — default — the response in result.structuredContent returns JSON
230
+ #> - text — in the response, serialized JSON is returned in result.content[0].text
231
+ answerAs: text
232
232
  #> When true, strips `annotations` from tool listings returned to clients
233
233
  #> (hides hints like readOnlyHint / destructiveHint). Default: false.
234
234
  hideAnnotations: false
@@ -34,11 +34,11 @@ logger:
34
34
 
35
35
  mcp:
36
36
  transportType: MCP_TRANSPORT_TYPE
37
- toolAnswerAs: MCP_TOOL_ANSWER_AS
38
37
  rateLimit:
39
38
  maxRequests: MCP_RATE_LIMIT_MAX_REQUESTS
40
39
  windowMs: MCP_RATE_LIMIT_WINDOW_MS
41
40
  tools:
41
+ answerAs: MCP_TOOLS_ANSWER_AS
42
42
  hideAnnotations: MCP_TOOLS_HIDE_ANNOTATIONS
43
43
 
44
44
  uiColor:
@@ -235,18 +235,18 @@ logger:
235
235
  mcp:
236
236
  #> Transport for the MCP server: stdio | http
237
237
  transportType: http
238
- #> Response format configuration.
239
- #> - structuredContent — default — the response in result.structuredContent returns JSON
240
- #> - text — in the response, serialized JSON is returned in result.content[0].text
241
- toolAnswerAs: text
242
238
  #> Per-client request rate limiting for the MCP endpoint
243
239
  rateLimit:
244
240
  #> Maximum number of requests allowed within windowMs
245
241
  maxRequests: 100
246
242
  #> Rate limit window length in milliseconds (1 minute)
247
243
  windowMs: 60000
248
- #> Tool listing behavior
244
+ #> Tool listing and response behavior
249
245
  tools:
246
+ #> Response format configuration.
247
+ #> - structuredContent — default — the response in result.structuredContent returns JSON
248
+ #> - text — in the response, serialized JSON is returned in result.content[0].text
249
+ answerAs: text
250
250
  #> When true, strips `annotations` from tool listings returned to clients
251
251
  #> (hides hints like readOnlyHint / destructiveHint). Default: false.
252
252
  hideAnnotations: false