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
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
97
|
-
- **`asTextContent()` / `asJson()`** — Direct formatting, ignores `
|
|
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
|
|
@@ -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/config/_local.yaml
CHANGED
|
@@ -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:
|
package/config/default.yaml
CHANGED
|
@@ -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
|