fa-mcp-sdk 0.11.10 → 0.11.14
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/cli-template/.claude/skills/create-mcp-wizard/SKILL.md +1 -1
- package/cli-template/.claude/skills/create-mcp-wizard/scripts/gitlab-push.js +3 -3
- package/cli-template/.env.example +48 -48
- package/cli-template/FA-MCP-SDK-DOC/02-1-tools-and-api.md +86 -0
- package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +20 -0
- package/cli-template/FA-MCP-SDK-DOC/12-implementation-standard.md +48 -3
- package/cli-template/package.json +1 -1
- package/dist/core/web/static/agent-tester/script.js +17 -0
- package/dist/core/web/static/agent-tester/styles.css +16 -2
- package/package.json +1 -1
- package/cli-template/.gitlab-ci.yml +0 -135
- package/cli-template/deploy/gitlab-runner/.env.example +0 -16
- package/cli-template/deploy/gitlab-runner/README.md +0 -65
- package/cli-template/deploy/gitlab-runner/config/config.toml.template +0 -26
- package/cli-template/deploy/gitlab-runner/docker-compose.yml +0 -39
- package/cli-template/deploy/gitlab-runner/entrypoint.sh +0 -27
- package/cli-template/deploy/gitlab-runner/start.sh +0 -47
|
@@ -185,7 +185,7 @@ with an auth error, surface it to the user; do not attempt API-token workarounds
|
|
|
185
185
|
Collect GitLab credentials — prefer values already in the accompanying text, ask only for what's
|
|
186
186
|
missing:
|
|
187
187
|
|
|
188
|
-
- `baseUrl` — e.g. `https://gitlab.
|
|
188
|
+
- `baseUrl` — e.g. `https://gitlab.corp.com/api/v4`
|
|
189
189
|
- `token` — GitLab private token with `api` scope
|
|
190
190
|
- `group` — group name or full path (e.g. `mcp-servers` or `ai/mcp`), OR `groupId` numeric
|
|
191
191
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Finally runs `git init / add / commit / remote add / push -u origin <branch>`.
|
|
8
8
|
*
|
|
9
9
|
* Environment / flags:
|
|
10
|
-
* --base-url <url> (e.g. https://gitlab.
|
|
10
|
+
* --base-url <url> (e.g. https://gitlab.corp.com/api/v4) — required
|
|
11
11
|
* --token <tok> GitLab private token — required
|
|
12
12
|
* --group <name> Group name or full path (e.g. "mcp-servers") — required unless --group-id is given
|
|
13
13
|
* --group-id <n> Numeric group id — overrides --group lookup
|
|
@@ -54,7 +54,7 @@ function die (msg, code = 1) {
|
|
|
54
54
|
process.exit(code);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
if (!baseUrl) die('Missing --base-url (or GITLAB_BASE_URL). Example: https://gitlab.
|
|
57
|
+
if (!baseUrl) die('Missing --base-url (or GITLAB_BASE_URL). Example: https://gitlab.corp.com/api/v4');
|
|
58
58
|
if (!token) die('Missing --token (or GITLAB_TOKEN).');
|
|
59
59
|
if (!projectName) die('Missing --name (project name).');
|
|
60
60
|
if (!groupId && !groupArg) die('Missing --group or --group-id.');
|
|
@@ -154,4 +154,4 @@ function gitPush (remoteUrl) {
|
|
|
154
154
|
} catch (e) {
|
|
155
155
|
die(e.message);
|
|
156
156
|
}
|
|
157
|
-
})();
|
|
157
|
+
})();
|
|
@@ -1,48 +1,48 @@
|
|
|
1
|
-
# SERVICE_NAME - service name. If this variable is not specified, it is taken from package.json.name
|
|
2
|
-
SERVICE_NAME={{project.name}}
|
|
3
|
-
# PRODUCT_NAME - If this variable is not specified, it is taken from package.json.productName
|
|
4
|
-
# PRODUCT_NAME=
|
|
5
|
-
|
|
6
|
-
NODE_ENV={{NODE_ENV}}
|
|
7
|
-
# Used for PM2
|
|
8
|
-
SERVICE_INSTANCE={{SERVICE_INSTANCE}}
|
|
9
|
-
PM2_NAMESPACE={{PM2_NAMESPACE}}
|
|
10
|
-
|
|
11
|
-
# Affects how the Consul service ID is formed - as a product or development ID
|
|
12
|
-
NODE_CONSUL_ENV={{NODE_CONSUL_ENV}}
|
|
13
|
-
|
|
14
|
-
DEBUG=config-info
|
|
15
|
-
# Used for PM2 service configuration
|
|
16
|
-
#┌────┬───────────────────────────────────────────────────────────┬─────────────────┬
|
|
17
|
-
#│ id │ name │ namespace │
|
|
18
|
-
#├────┼───────────────────────────────────────────────────────────┼─────────────────┼
|
|
19
|
-
#│ 1 │ <SERVICE_NAME | package.json.name>[--<SERVICE_INSTANCE>] │ <PM2_NAMESPACE> │
|
|
20
|
-
|
|
21
|
-
## DEBUG patterns ##
|
|
22
|
-
# AP-UPDATER - consul/access-points-updater
|
|
23
|
-
# fa-consul:reg | fa-consul:* - consul/cyclic-register
|
|
24
|
-
# fa-consul:curl - consul/prepare-consul-api
|
|
25
|
-
# token:auth - authentication: which auth method matched, token parsing/validation outcome
|
|
26
|
-
# mcp:tool - tools/call: tool name + arguments in, response (text or JSON) out
|
|
27
|
-
# mcp:resource - resources/list and resources/read: URI in, body out
|
|
28
|
-
# mcp:notification - all incoming notifications/* (method + params)
|
|
29
|
-
# mcp:prompt - prompts/list and prompts/get: name/args in, messages out
|
|
30
|
-
# mcp:* - all four MCP channels above at once
|
|
31
|
-
# mcp-handshake - HTTP transport: per-request dump (method, id, session routing, protocol, auth, IP)
|
|
32
|
-
# mcp-rpc - HTTP transport: one-line summary of every successful JSON-RPC response
|
|
33
|
-
#
|
|
34
|
-
#(session-lifecycle events, the -32600 "no valid session" rejection and JSON-RPC errors log always)
|
|
35
|
-
#
|
|
36
|
-
# ========================================================================
|
|
37
|
-
# AGENT TESTER - Built-in AI agent for testing MCP tools
|
|
38
|
-
# ========================================================================
|
|
39
|
-
# AGENT_TESTER_ENABLED=true
|
|
40
|
-
# AGENT_TESTER_USE_AUTH=false
|
|
41
|
-
# AGENT_TESTER_OPENAI_API_KEY=sk-...
|
|
42
|
-
# AGENT_TESTER_OPENAI_BASE_URL=
|
|
43
|
-
|
|
44
|
-
# The address of the mcp server for testing. Default: http://localhost:<config.webServer.port>
|
|
45
|
-
# TEST_MCP_SERVER_URL=
|
|
46
|
-
|
|
47
|
-
# The directory to store test result logs
|
|
48
|
-
TEST_RESULT_LOGS_DIR='_logs/mcp'
|
|
1
|
+
# SERVICE_NAME - service name. If this variable is not specified, it is taken from package.json.name
|
|
2
|
+
SERVICE_NAME={{project.name}}
|
|
3
|
+
# PRODUCT_NAME - If this variable is not specified, it is taken from package.json.productName
|
|
4
|
+
# PRODUCT_NAME=
|
|
5
|
+
|
|
6
|
+
NODE_ENV={{NODE_ENV}}
|
|
7
|
+
# Used for PM2
|
|
8
|
+
SERVICE_INSTANCE={{SERVICE_INSTANCE}}
|
|
9
|
+
PM2_NAMESPACE={{PM2_NAMESPACE}}
|
|
10
|
+
|
|
11
|
+
# Affects how the Consul service ID is formed - as a product or development ID
|
|
12
|
+
NODE_CONSUL_ENV={{NODE_CONSUL_ENV}}
|
|
13
|
+
|
|
14
|
+
DEBUG=config-info
|
|
15
|
+
# Used for PM2 service configuration
|
|
16
|
+
#┌────┬───────────────────────────────────────────────────────────┬─────────────────┬
|
|
17
|
+
#│ id │ name │ namespace │
|
|
18
|
+
#├────┼───────────────────────────────────────────────────────────┼─────────────────┼
|
|
19
|
+
#│ 1 │ <SERVICE_NAME | package.json.name>[--<SERVICE_INSTANCE>] │ <PM2_NAMESPACE> │
|
|
20
|
+
|
|
21
|
+
## DEBUG patterns ##
|
|
22
|
+
# AP-UPDATER - consul/access-points-updater
|
|
23
|
+
# fa-consul:reg | fa-consul:* - consul/cyclic-register
|
|
24
|
+
# fa-consul:curl - consul/prepare-consul-api
|
|
25
|
+
# token:auth - authentication: which auth method matched, token parsing/validation outcome
|
|
26
|
+
# mcp:tool - tools/call: tool name + arguments in, response (text or JSON) out
|
|
27
|
+
# mcp:resource - resources/list and resources/read: URI in, body out
|
|
28
|
+
# mcp:notification - all incoming notifications/* (method + params)
|
|
29
|
+
# mcp:prompt - prompts/list and prompts/get: name/args in, messages out
|
|
30
|
+
# mcp:* - all four MCP channels above at once
|
|
31
|
+
# mcp-handshake - HTTP transport: per-request dump (method, id, session routing, protocol, auth, IP)
|
|
32
|
+
# mcp-rpc - HTTP transport: one-line summary of every successful JSON-RPC response
|
|
33
|
+
#
|
|
34
|
+
#(session-lifecycle events, the -32600 "no valid session" rejection and JSON-RPC errors log always)
|
|
35
|
+
#
|
|
36
|
+
# ========================================================================
|
|
37
|
+
# AGENT TESTER - Built-in AI agent for testing MCP tools
|
|
38
|
+
# ========================================================================
|
|
39
|
+
# AGENT_TESTER_ENABLED=true
|
|
40
|
+
# AGENT_TESTER_USE_AUTH=false
|
|
41
|
+
# AGENT_TESTER_OPENAI_API_KEY=sk-...
|
|
42
|
+
# AGENT_TESTER_OPENAI_BASE_URL=
|
|
43
|
+
|
|
44
|
+
# The address of the mcp server for testing. Default: http://localhost:<config.webServer.port>
|
|
45
|
+
# TEST_MCP_SERVER_URL=
|
|
46
|
+
|
|
47
|
+
# The directory to store test result logs
|
|
48
|
+
TEST_RESULT_LOGS_DIR='_logs/mcp'
|
|
@@ -172,6 +172,92 @@ asJsonError({ code: 'NOT_FOUND', key: 'X' }); // { structuredContent: {...},
|
|
|
172
172
|
> for missing resources, convert those branches to `return formatToolError('Not found: ...')`. The
|
|
173
173
|
> LLM will start surfacing "Such an issue does not exist" to the user instead of failing the call.
|
|
174
174
|
|
|
175
|
+
### Normalizing upstream API errors
|
|
176
|
+
|
|
177
|
+
The `isError` vs `throw` decision above is easy when the handler discovers the problem itself (a `null`
|
|
178
|
+
issue). It is harder when the failure surfaces as a raw error thrown deep inside an HTTP client — a 404
|
|
179
|
+
from the upstream API arrives as an Axios/`fetch` rejection, not as a clean `formatToolError`. Catching
|
|
180
|
+
that in every handler is repetitive and easy to get wrong. The pattern below centralizes it in the single
|
|
181
|
+
`catch` of `handleToolCall`, and implements standard
|
|
182
|
+
[§13.4 "Mapping upstream errors"](./12-implementation-standard.md#134-mapping-upstream-downstream-api-errors).
|
|
183
|
+
|
|
184
|
+
It has three pure steps — translate, classify, surface:
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
import {
|
|
188
|
+
formatToolError, ToolExecutionError, ServerError, RateLimitedError,
|
|
189
|
+
UpstreamUnavailableError, ValidationError, ConflictError, ResourceNotFoundError, toStr,
|
|
190
|
+
} from 'fa-mcp-sdk';
|
|
191
|
+
|
|
192
|
+
// 1. TRANSLATE — convert a raw upstream HTTP error into a typed error class (no throw here).
|
|
193
|
+
// Map the upstream status onto the Appendix B error set instead of one opaque ServerError.
|
|
194
|
+
function handleAxiosError(error: any, toolName: string): never {
|
|
195
|
+
const status = error?.response?.status;
|
|
196
|
+
const msg = extractUpstreamMessage(error?.response?.data) ?? error?.message ?? 'Unknown error';
|
|
197
|
+
const data = { toolName, status }; // safe: no body, no headers, no stack
|
|
198
|
+
|
|
199
|
+
if (!status || status >= 502) throw new UpstreamUnavailableError(`Upstream unavailable: ${msg}`, data);
|
|
200
|
+
if (status === 400) throw new ValidationError(`Invalid request: ${msg}`);
|
|
201
|
+
if (status === 404) throw new ResourceNotFoundError(msg, data);
|
|
202
|
+
if (status === 409) throw new ConflictError(`State conflict: ${msg}`, data);
|
|
203
|
+
if (status === 429) {
|
|
204
|
+
const retryAfter = parseInt(error?.response?.headers?.['retry-after'], 10) || 60;
|
|
205
|
+
throw new RateLimitedError(`Rate limited: ${msg}`, retryAfter);
|
|
206
|
+
}
|
|
207
|
+
// 401/403 and other 5xx — keep the upstream status in `data.status` so step 2 can recognize it.
|
|
208
|
+
throw new ServerError(`Upstream error (HTTP ${status}): ${msg}`, data);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 2. NORMALIZE — turn ANY thrown value into a concrete Error, still WITHOUT throwing.
|
|
212
|
+
// A pure function lets the MCP path (may surface to the LLM) and a REST path (always throws)
|
|
213
|
+
// share one step.
|
|
214
|
+
export function normalizeToolError(error: any, toolName: string): Error {
|
|
215
|
+
if (error instanceof ToolExecutionError || error instanceof ServerError ||
|
|
216
|
+
typeof error?.jsonRpcCode === 'number') {
|
|
217
|
+
return error; // already a domain error
|
|
218
|
+
}
|
|
219
|
+
if (isAxiosError(error)) {
|
|
220
|
+
try { handleAxiosError(error, toolName); } catch (converted) { return converted as Error; }
|
|
221
|
+
}
|
|
222
|
+
return new ServerError(toStr(error), { toolName }, true); // catch-all, sanitized (no upstream status)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 3. CLASSIFY — decide whether the model should SEE the message (isError) or get a thrown protocol error.
|
|
226
|
+
export function isLlmVisibleError(error: any): boolean {
|
|
227
|
+
if (error instanceof RateLimitedError) return false; // retry contract — keep -32003 thrown
|
|
228
|
+
if (error instanceof ToolExecutionError) return true; // JQL/validation written for the model
|
|
229
|
+
if (typeof error?.jsonRpcCode === 'number') return true; // ValidationError/NotFound/Conflict/Upstream
|
|
230
|
+
if (error instanceof ServerError && error?.details?.status != null) return true; // upstream 401/403/5xx
|
|
231
|
+
return false; // catch-all ServerError → "Internal error"
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Wire all three into the single `catch`, so every handler benefits without its own try/catch:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
} catch (error: any) {
|
|
239
|
+
const normalized = normalizeToolError(error, toolName);
|
|
240
|
+
if (isLlmVisibleError(normalized)) {
|
|
241
|
+
// The model reads the upstream reason ("Issue AITECH-123 does not exist") and self-corrects.
|
|
242
|
+
return formatToolError(normalized.message);
|
|
243
|
+
}
|
|
244
|
+
throw normalized; // RateLimitedError / internal → protocol error
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Why this split matters:
|
|
249
|
+
|
|
250
|
+
- A **404 raised by the upstream API** becomes `ResourceNotFoundError` (numeric `jsonRpcCode`), so
|
|
251
|
+
`isLlmVisibleError` returns `true` and the model gets `result.isError=true` — exactly like the manual
|
|
252
|
+
`formatToolError` branch in the previous section, but for an error it never saw directly.
|
|
253
|
+
- **`RateLimitedError` stays thrown** as `-32003` with `retryAfter` — clients depend on that contract, so
|
|
254
|
+
it must not collapse into an `isError` text result.
|
|
255
|
+
- A **catch-all `ServerError`** (no `details.status`) stays thrown and is sanitized by the SDK to
|
|
256
|
+
`Internal error` — its text may carry internal detail and MUST NOT reach the model (standard §13.3).
|
|
257
|
+
|
|
258
|
+
> Keep `normalizeToolError` **pure** (never throws). A throwing normalizer forces every call site into its
|
|
259
|
+
> own try/catch and defeats the point of centralizing the logic.
|
|
260
|
+
|
|
175
261
|
### Headers Access
|
|
176
262
|
|
|
177
263
|
Headers are normalized to lowercase. Available in HTTP/SSE transports:
|
|
@@ -37,6 +37,26 @@ class MyError extends BaseMcpError {
|
|
|
37
37
|
| `RateLimitedError` | `RATE_LIMITED` | `-32003` | 429 | Appendix B |
|
|
38
38
|
| `TimeoutError` | `TIMEOUT` | `-32004` | 504 | Appendix B |
|
|
39
39
|
| `PayloadTooLargeError` | `PAYLOAD_TOO_LARGE` | `-32005` | 413 | Appendix B |
|
|
40
|
+
| `UpstreamUnavailableError` | `UPSTREAM_UNAVAILABLE` | `-32006` | 503 | Appendix B |
|
|
41
|
+
| `ConflictError` | `CONFLICT` | `-32007` | 409 | Appendix B |
|
|
42
|
+
|
|
43
|
+
### Mapping a Downstream API Status to a Typed Error
|
|
44
|
+
|
|
45
|
+
When a tool proxies a downstream HTTP API, translate the upstream status into one of these classes instead
|
|
46
|
+
of a single opaque `ServerError`. This gives the JSON-RPC layer a meaningful code and lets the surfacing
|
|
47
|
+
logic (next) decide whether the model should see the message. This is standard §13.4; the end-to-end
|
|
48
|
+
`normalizeToolError` / `isLlmVisibleError` / `formatToolError` pattern is in
|
|
49
|
+
[02-1-tools-and-api.md → "Normalizing upstream API errors"](./02-1-tools-and-api.md).
|
|
50
|
+
|
|
51
|
+
| Upstream HTTP | Throw | Surfaced to model as `isError`? |
|
|
52
|
+
|-------------------------------|-----------------------------|---------------------------------|
|
|
53
|
+
| 400 | `ValidationError` | yes |
|
|
54
|
+
| 401 / 403 | `ServerError` (status in data) | yes |
|
|
55
|
+
| 404 | `ResourceNotFoundError` | yes |
|
|
56
|
+
| 409 | `ConflictError` | yes |
|
|
57
|
+
| 429 | `RateLimitedError` | no — thrown, keeps `retryAfter` |
|
|
58
|
+
| 502 / 503 / 504 / no response | `UpstreamUnavailableError` | yes |
|
|
59
|
+
| other 5xx / unexpected | `ServerError` (no status) | no — thrown, sanitized |
|
|
40
60
|
|
|
41
61
|
## Error Utilities
|
|
42
62
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
| Parameter | Value |
|
|
4
4
|
|----------------------------|------------------------------------|
|
|
5
|
-
| Version | 1.
|
|
5
|
+
| Version | 1.3 |
|
|
6
6
|
| Status | Active |
|
|
7
|
-
| Date | 2026-06-
|
|
7
|
+
| Date | 2026-06-05 |
|
|
8
8
|
| Scope | All internal company MCP servers |
|
|
9
9
|
| Base MCP | MCP 2025-11-25 |
|
|
10
10
|
| Starter SDK (optional) | `fa-mcp-sdk` |
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
> This document is the English translation of the corporate implementation standard. It restates the
|
|
14
14
|
> explicit MCP 2025-11-25 requirements plus the corporate Avatar / AI Platform profile in a single
|
|
15
15
|
> self-contained document. Version 1.2 adds the side-effect tools and risk-level rules and fixes the
|
|
16
|
-
> `-32007` error-code inconsistency.
|
|
16
|
+
> `-32007` error-code inconsistency. Version 1.3 adds §13.4 on mapping upstream (downstream API) errors
|
|
17
|
+
> to typed error classes and the rule for surfacing model-correctable errors via `result.isError=true`.
|
|
17
18
|
|
|
18
19
|
## Table of Contents
|
|
19
20
|
|
|
@@ -765,6 +766,50 @@ An error returned externally MUST NOT contain:
|
|
|
765
766
|
- raw SQL/expression text with user data;
|
|
766
767
|
- internal service names that are not part of the public contract.
|
|
767
768
|
|
|
769
|
+
### 13.4. Mapping upstream (downstream API) errors
|
|
770
|
+
|
|
771
|
+
This section is a corporate recommendation for servers that proxy a downstream HTTP API (Jira, GitLab, an
|
|
772
|
+
internal microservice, etc.). It defines how a failed upstream call is translated into the two error types
|
|
773
|
+
of §13.1 so that the model receives an actionable reason instead of one opaque `-32603 Internal error`.
|
|
774
|
+
|
|
775
|
+
When a tool calls a downstream API, the server SHOULD translate the upstream HTTP status into the matching
|
|
776
|
+
typed error class from [Appendix B](#appendix-b-error-codes) rather than collapsing every failure into a
|
|
777
|
+
generic internal error. The recommended mapping is:
|
|
778
|
+
|
|
779
|
+
| Upstream HTTP | Typed error class | JSON-RPC | Returned to the model |
|
|
780
|
+
| --------------------------------- | ---------------------- | -------- | --------------------- |
|
|
781
|
+
| 400 | `ValidationError` | -32602 | `isError=true` |
|
|
782
|
+
| 401 / 403 | `ServerError` (with upstream status in `data`) | -32000 | `isError=true` |
|
|
783
|
+
| 404 | `ResourceNotFoundError`| -32002 | `isError=true` |
|
|
784
|
+
| 409 | `ConflictError` | -32007 | `isError=true` |
|
|
785
|
+
| 429 | `RateLimitedError` | -32003 | thrown (see below) |
|
|
786
|
+
| 502 / 503 / 504 / no response | `UpstreamUnavailableError` | -32006 | `isError=true` |
|
|
787
|
+
| other 5xx | `ServerError` | -32000 | thrown |
|
|
788
|
+
|
|
789
|
+
The decision whether to surface an error to the model or to throw it follows three rules:
|
|
790
|
+
|
|
791
|
+
- An error whose message is **safe to expose and actionable** — built from the structured upstream error
|
|
792
|
+
body, not from internal state — SHOULD be returned as a tool execution result with `result.isError=true`
|
|
793
|
+
(§9.4, §13.1). The model reads the upstream reason (for example `Issue AITECH-123 does not exist`) and
|
|
794
|
+
self-corrects instead of treating the call as a hard sandbox failure. A `404` raised by the downstream
|
|
795
|
+
API is the canonical case: it MUST reach the model as `result.isError=true`, not as a thrown protocol
|
|
796
|
+
error.
|
|
797
|
+
- `-32003 Rate limited` MUST remain a **thrown** protocol error and MUST carry the `Retry-After` header /
|
|
798
|
+
`retryAfter` value (§14, Appendix B.3). It MUST NOT be flattened into an `isError` text result, because
|
|
799
|
+
clients rely on the numeric code and the retry hint to schedule a retry.
|
|
800
|
+
- An internal failure with **no upstream status** (the catch-all wrapper around an unexpected exception)
|
|
801
|
+
MUST stay a thrown protocol error and MUST be sanitized per §13.3 — typically `-32603 Internal error`
|
|
802
|
+
with no stack trace and no secrets.
|
|
803
|
+
|
|
804
|
+
A reference implementation of this pattern — a pure `normalizeToolError()` that converts any thrown value
|
|
805
|
+
into a typed error without throwing, an `isLlmVisibleError()` predicate that applies the three rules above,
|
|
806
|
+
and the `formatToolError()` call that surfaces the message — is documented in
|
|
807
|
+
[02-1-tools-and-api.md → "Normalizing upstream API errors"](./02-1-tools-and-api.md).
|
|
808
|
+
|
|
809
|
+
Whatever message is exposed (via `isError=true` or a thrown error) MUST still satisfy the §13.3
|
|
810
|
+
prohibitions: the upstream error body is forwarded only after it has been reduced to its human-readable
|
|
811
|
+
text, never as a raw payload that could carry internal paths, tokens, or stack traces.
|
|
812
|
+
|
|
768
813
|
## 14. Limits and protection
|
|
769
814
|
|
|
770
815
|
Each server MUST document and enforce:
|
|
@@ -800,6 +800,22 @@ class McpAgentTester {
|
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
this.refreshToolListAppIcons();
|
|
803
|
+
this.applyAppModeVisibility();
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Show the Inspector tab only while MCP Apps mode is ON. When the mode is
|
|
808
|
+
* turned off while the Inspector tab is active, fall back to the Chat tab so
|
|
809
|
+
* the user is not left on a hidden pane.
|
|
810
|
+
*/
|
|
811
|
+
applyAppModeVisibility() {
|
|
812
|
+
const appEl = document.querySelector('.app');
|
|
813
|
+
if (appEl) {
|
|
814
|
+
appEl.classList.toggle('apps-mode-on', !!this.appMode);
|
|
815
|
+
}
|
|
816
|
+
if (!this.appMode && this.activeTab === 'inspector') {
|
|
817
|
+
this.switchTab('chat');
|
|
818
|
+
}
|
|
803
819
|
}
|
|
804
820
|
|
|
805
821
|
/**
|
|
@@ -1395,6 +1411,7 @@ class McpAgentTester {
|
|
|
1395
1411
|
this.appModeToggle.addEventListener('change', () => this.handleAppModeToggle());
|
|
1396
1412
|
this.updateAppModeToggleAvailability();
|
|
1397
1413
|
}
|
|
1414
|
+
this.applyAppModeVisibility();
|
|
1398
1415
|
|
|
1399
1416
|
this.mcpConnectionForm.addEventListener('submit', (e) => this.handleMcpConnection(e));
|
|
1400
1417
|
|
|
@@ -1110,6 +1110,17 @@ body {
|
|
|
1110
1110
|
.inspector-section {
|
|
1111
1111
|
padding: 8px 14px 14px;
|
|
1112
1112
|
border-bottom: 1px solid var(--border);
|
|
1113
|
+
min-height: 0;
|
|
1114
|
+
overflow-y: auto;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
/* App Tools list gets the larger share; UI Resources the smaller. Both scroll. */
|
|
1118
|
+
.inspector-tools .inspector-section:nth-of-type(1) {
|
|
1119
|
+
flex: 2 1 0;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
.inspector-tools .inspector-section:nth-of-type(2) {
|
|
1123
|
+
flex: 1 1 0;
|
|
1113
1124
|
}
|
|
1114
1125
|
|
|
1115
1126
|
.inspector-section h4 {
|
|
@@ -1259,8 +1270,11 @@ body {
|
|
|
1259
1270
|
}
|
|
1260
1271
|
|
|
1261
1272
|
/* Inspector tab visibility tied to MCP Apps mode */
|
|
1262
|
-
.app
|
|
1263
|
-
|
|
1273
|
+
.app-only-tab {
|
|
1274
|
+
display: none;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
.app.apps-mode-on .app-only-tab {
|
|
1264
1278
|
display: inline-flex;
|
|
1265
1279
|
}
|
|
1266
1280
|
|
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.11.
|
|
4
|
+
"version": "0.11.14",
|
|
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",
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
stages:
|
|
2
|
-
- lint
|
|
3
|
-
- test-build
|
|
4
|
-
- build-image
|
|
5
|
-
- deploy
|
|
6
|
-
|
|
7
|
-
# ── CI: Merge Request checks ──────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
.node-template: &node-template
|
|
10
|
-
image: node:22-alpine
|
|
11
|
-
tags:
|
|
12
|
-
- docker
|
|
13
|
-
before_script:
|
|
14
|
-
- yarn install --frozen-lockfile
|
|
15
|
-
cache:
|
|
16
|
-
key:
|
|
17
|
-
files:
|
|
18
|
-
- yarn.lock
|
|
19
|
-
paths:
|
|
20
|
-
- node_modules/
|
|
21
|
-
rules:
|
|
22
|
-
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
|
|
23
|
-
|
|
24
|
-
lint:
|
|
25
|
-
<<: *node-template
|
|
26
|
-
stage: lint
|
|
27
|
-
script:
|
|
28
|
-
- yarn lint
|
|
29
|
-
|
|
30
|
-
test-build:
|
|
31
|
-
<<: *node-template
|
|
32
|
-
stage: test-build
|
|
33
|
-
script:
|
|
34
|
-
- yarn build
|
|
35
|
-
|
|
36
|
-
# ── CD: Build Docker Image ────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
build-image:
|
|
39
|
-
stage: build-image
|
|
40
|
-
image: docker:27
|
|
41
|
-
services:
|
|
42
|
-
- docker:27-dind
|
|
43
|
-
tags:
|
|
44
|
-
- docker
|
|
45
|
-
script:
|
|
46
|
-
- docker compose -f deploy/docker/docker-compose.yml up -d --build --force-recreate app
|
|
47
|
-
rules:
|
|
48
|
-
- if: '$CI_COMMIT_BRANCH == "master"'
|
|
49
|
-
when: manual
|
|
50
|
-
|
|
51
|
-
# ── CD: Deploy ────────────────────────────────────────────────────
|
|
52
|
-
# Замените <SERVER_TAG> на тег вашего сервера, <BRANCH> на целевую ветку,
|
|
53
|
-
# <DEPLOY_DIR> на путь к проекту на сервере.
|
|
54
|
-
|
|
55
|
-
# deploy_<instance>:
|
|
56
|
-
# stage: deploy
|
|
57
|
-
# tags:
|
|
58
|
-
# - <SERVER_TAG>
|
|
59
|
-
# variables:
|
|
60
|
-
# HOST_PORT: {{port}}
|
|
61
|
-
# environment:
|
|
62
|
-
# name: <instance>
|
|
63
|
-
# script:
|
|
64
|
-
# - docker compose -f deploy/docker/docker-compose.yml --env-file .env up -d --build --force-recreate app
|
|
65
|
-
# - |
|
|
66
|
-
# echo "Waiting for health check..."
|
|
67
|
-
# for i in $(seq 1 60); do
|
|
68
|
-
# if docker compose -f deploy/docker/docker-compose.yml exec -T app wget -q -O /dev/null http://localhost:{{port}}/health 2>/dev/null; then
|
|
69
|
-
# echo "Health check passed after ${i}s"
|
|
70
|
-
# break
|
|
71
|
-
# fi
|
|
72
|
-
# if [ "$i" -eq 60 ]; then
|
|
73
|
-
# echo "Health check failed after 60s"
|
|
74
|
-
# docker compose -f deploy/docker/docker-compose.yml logs --tail=50 app
|
|
75
|
-
# exit 1
|
|
76
|
-
# fi
|
|
77
|
-
# sleep 1
|
|
78
|
-
# done
|
|
79
|
-
# - docker compose -f deploy/docker/docker-compose.yml logs --tail=50 app
|
|
80
|
-
# rules:
|
|
81
|
-
# - if: '$CI_COMMIT_BRANCH == "<BRANCH>"'
|
|
82
|
-
# when: manual
|
|
83
|
-
|
|
84
|
-
# stop_<instance>:
|
|
85
|
-
# stage: deploy
|
|
86
|
-
# tags:
|
|
87
|
-
# - <SERVER_TAG>
|
|
88
|
-
# environment:
|
|
89
|
-
# name: <instance>
|
|
90
|
-
# script:
|
|
91
|
-
# - docker compose -f deploy/docker/docker-compose.yml down
|
|
92
|
-
# rules:
|
|
93
|
-
# - if: '$CI_COMMIT_BRANCH == "<BRANCH>"'
|
|
94
|
-
# when: manual
|
|
95
|
-
|
|
96
|
-
# restart_<instance>:
|
|
97
|
-
# stage: deploy
|
|
98
|
-
# tags:
|
|
99
|
-
# - <SERVER_TAG>
|
|
100
|
-
# variables:
|
|
101
|
-
# HOST_PORT: {{port}}
|
|
102
|
-
# environment:
|
|
103
|
-
# name: <instance>
|
|
104
|
-
# script:
|
|
105
|
-
# - docker compose -f deploy/docker/docker-compose.yml --env-file .env restart app
|
|
106
|
-
# - |
|
|
107
|
-
# echo "Waiting for health check..."
|
|
108
|
-
# for i in $(seq 1 60); do
|
|
109
|
-
# if docker compose -f deploy/docker/docker-compose.yml exec -T app wget -q -O /dev/null http://localhost:{{port}}/health 2>/dev/null; then
|
|
110
|
-
# echo "Health check passed after ${i}s"
|
|
111
|
-
# break
|
|
112
|
-
# fi
|
|
113
|
-
# if [ "$i" -eq 60 ]; then
|
|
114
|
-
# echo "Health check failed after 60s"
|
|
115
|
-
# docker compose -f deploy/docker/docker-compose.yml logs --tail=100 app
|
|
116
|
-
# exit 1
|
|
117
|
-
# fi
|
|
118
|
-
# sleep 1
|
|
119
|
-
# done
|
|
120
|
-
# - docker compose -f deploy/docker/docker-compose.yml logs --tail=100 app
|
|
121
|
-
# rules:
|
|
122
|
-
# - if: '$CI_COMMIT_BRANCH == "<BRANCH>"'
|
|
123
|
-
# when: manual
|
|
124
|
-
|
|
125
|
-
# logs_<instance>:
|
|
126
|
-
# stage: deploy
|
|
127
|
-
# tags:
|
|
128
|
-
# - <SERVER_TAG>
|
|
129
|
-
# environment:
|
|
130
|
-
# name: <instance>
|
|
131
|
-
# script:
|
|
132
|
-
# - docker compose -f deploy/docker/docker-compose.yml logs --tail=100 app
|
|
133
|
-
# rules:
|
|
134
|
-
# - if: '$CI_COMMIT_BRANCH == "<BRANCH>"'
|
|
135
|
-
# when: manual
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
# ── GitLab Runner version ─────────────────────────────────
|
|
2
|
-
GITLAB_VERSION=v18.10.3
|
|
3
|
-
HELPER_VERSION=v18.10.3
|
|
4
|
-
|
|
5
|
-
# ── GitLab connection ─────────────────────────────────────
|
|
6
|
-
GITLAB_URL=https://gitlab.finam.ru/
|
|
7
|
-
RUNNER_TAGS={{project.name}},docker
|
|
8
|
-
|
|
9
|
-
# ── Runner identity ──────────────────────────────────────
|
|
10
|
-
RUNNER_NAME={{project.name}}
|
|
11
|
-
RUNNER_TOKEN=
|
|
12
|
-
|
|
13
|
-
# ── Project directory on host ─────────────────────────────
|
|
14
|
-
# Absolute or relative path to the project root on the server.
|
|
15
|
-
# Defaults to ../../ (two levels up from this directory).
|
|
16
|
-
# PROJECT_DIR=/var/opt/node/{{project.name}}
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# GitLab Runner Setup
|
|
2
|
-
|
|
3
|
-
Docker-based GitLab Runner for CI/CD pipelines. Uses Docker executor with Docker-out-of-Docker (DooD) pattern.
|
|
4
|
-
|
|
5
|
-
## How It Works
|
|
6
|
-
|
|
7
|
-
1. `start.sh` validates `.env`, resolves `PROJECT_DIR`, starts the runner container
|
|
8
|
-
2. `entrypoint.sh` substitutes variables in `config.toml.template` → `config.toml`, starts `gitlab-runner run`
|
|
9
|
-
3. Runner registers with GitLab, listens for CI jobs matching its tags
|
|
10
|
-
|
|
11
|
-
## Setup
|
|
12
|
-
|
|
13
|
-
```bash
|
|
14
|
-
# 1. Copy environment template
|
|
15
|
-
cp .env.example .env
|
|
16
|
-
|
|
17
|
-
# 2. Get runner token from GitLab UI:
|
|
18
|
-
# Settings → CI/CD → Runners → New project runner
|
|
19
|
-
# Set tags, then copy the token
|
|
20
|
-
|
|
21
|
-
# 3. Fill .env with your token and settings
|
|
22
|
-
# Required: GITLAB_URL, RUNNER_NAME, RUNNER_TOKEN
|
|
23
|
-
|
|
24
|
-
# 4. Start the runner
|
|
25
|
-
bash start.sh
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Environment Variables
|
|
29
|
-
|
|
30
|
-
| Variable | Required | Default | Description |
|
|
31
|
-
|----------|----------|---------|-------------|
|
|
32
|
-
| `GITLAB_VERSION` | yes | v18.10.3 | GitLab Runner Docker image version |
|
|
33
|
-
| `HELPER_VERSION` | yes | v18.10.3 | GitLab Runner Helper image version |
|
|
34
|
-
| `GITLAB_URL` | yes | — | GitLab instance URL |
|
|
35
|
-
| `RUNNER_TAGS` | no | {{project.name}},docker | Comma-separated tags for job matching |
|
|
36
|
-
| `RUNNER_NAME` | yes | — | Runner hostname and container name |
|
|
37
|
-
| `RUNNER_TOKEN` | yes | — | Authentication token from GitLab UI |
|
|
38
|
-
| `PROJECT_DIR` | no | ../../ | Absolute path to project on host |
|
|
39
|
-
|
|
40
|
-
## Management Commands
|
|
41
|
-
|
|
42
|
-
```bash
|
|
43
|
-
# Start
|
|
44
|
-
bash start.sh
|
|
45
|
-
|
|
46
|
-
# View logs
|
|
47
|
-
docker compose logs -f
|
|
48
|
-
|
|
49
|
-
# Check status
|
|
50
|
-
docker compose exec gitlab-runner gitlab-runner status
|
|
51
|
-
|
|
52
|
-
# Stop
|
|
53
|
-
docker compose down
|
|
54
|
-
|
|
55
|
-
# Restart
|
|
56
|
-
docker compose restart
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
## Infrastructure
|
|
60
|
-
|
|
61
|
-
- **Network:** `gitlab-network` (bridge)
|
|
62
|
-
- **DNS:** Corporate DNS servers (10.77.96.10, 10.77.196.10)
|
|
63
|
-
- **Executor:** Docker with `docker:27` default image
|
|
64
|
-
- **Volumes:** Docker socket, daemon.json, project directory, /cache
|
|
65
|
-
- **Healthcheck:** `gitlab-runner status` every 60s
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
concurrent = 2
|
|
2
|
-
check_interval = 0
|
|
3
|
-
shutdown_timeout = 0
|
|
4
|
-
sentry_dsn = ""
|
|
5
|
-
|
|
6
|
-
[session_server]
|
|
7
|
-
session_timeout = 1800
|
|
8
|
-
|
|
9
|
-
[[runners]]
|
|
10
|
-
name = "__RUNNER_NAME__"
|
|
11
|
-
url = "__GITLAB_URL__"
|
|
12
|
-
token = "__RUNNER_TOKEN__"
|
|
13
|
-
executor = "docker"
|
|
14
|
-
tag_list = "__RUNNER_TAGS__"
|
|
15
|
-
[runners.docker]
|
|
16
|
-
tls_verify = false
|
|
17
|
-
image = "docker:27"
|
|
18
|
-
privileged = true
|
|
19
|
-
disable_entrypoint_overwrite = false
|
|
20
|
-
oom_kill_disable = false
|
|
21
|
-
disable_cache = false
|
|
22
|
-
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/etc/docker/daemon.json:/etc/docker/daemon.json", "__PROJECT_DIR__:__PROJECT_DIR__", "/cache"]
|
|
23
|
-
shm_size = 0
|
|
24
|
-
network_mtu = 0
|
|
25
|
-
pull_policy = ["if-not-present"]
|
|
26
|
-
helper_image = "gitlab/gitlab-runner-helper:__HELPER_VERSION__"
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
name: {{project.name}}-runner
|
|
2
|
-
|
|
3
|
-
services:
|
|
4
|
-
gitlab-runner:
|
|
5
|
-
image: gitlab/gitlab-runner:${GITLAB_VERSION}
|
|
6
|
-
hostname: ${RUNNER_NAME}
|
|
7
|
-
container_name: ${RUNNER_NAME}
|
|
8
|
-
environment:
|
|
9
|
-
- TZ=Europe/Moscow
|
|
10
|
-
- RUNNER_NAME=${RUNNER_NAME}
|
|
11
|
-
- RUNNER_TOKEN=${RUNNER_TOKEN}
|
|
12
|
-
- PROJECT_DIR=${PROJECT_DIR}
|
|
13
|
-
- GITLAB_URL=${GITLAB_URL}
|
|
14
|
-
- GITLAB_VERSION=${GITLAB_VERSION}
|
|
15
|
-
- RUNNER_TAGS=${RUNNER_TAGS}
|
|
16
|
-
- HELPER_VERSION=${HELPER_VERSION}
|
|
17
|
-
dns:
|
|
18
|
-
- 10.77.96.10
|
|
19
|
-
- 10.77.196.10
|
|
20
|
-
restart: always
|
|
21
|
-
entrypoint: ["/bin/sh", "/entrypoint.sh"]
|
|
22
|
-
volumes:
|
|
23
|
-
- './config:/etc/gitlab-runner'
|
|
24
|
-
- './entrypoint.sh:/entrypoint.sh:ro'
|
|
25
|
-
- '/var/run/docker.sock:/var/run/docker.sock'
|
|
26
|
-
- '/etc/docker/daemon.json:/etc/docker/daemon.json'
|
|
27
|
-
healthcheck:
|
|
28
|
-
test: ["CMD", "gitlab-runner", "status"]
|
|
29
|
-
interval: 1m
|
|
30
|
-
timeout: 10s
|
|
31
|
-
retries: 3
|
|
32
|
-
start_period: 30s
|
|
33
|
-
networks:
|
|
34
|
-
- gitlab
|
|
35
|
-
|
|
36
|
-
networks:
|
|
37
|
-
gitlab:
|
|
38
|
-
name: gitlab-network
|
|
39
|
-
driver: bridge
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/bin/sh
|
|
2
|
-
set -e
|
|
3
|
-
|
|
4
|
-
TEMPLATE="/etc/gitlab-runner/config.toml.template"
|
|
5
|
-
CONFIG="/etc/gitlab-runner/config.toml"
|
|
6
|
-
|
|
7
|
-
if [ -z "$RUNNER_TOKEN" ]; then
|
|
8
|
-
echo "ERROR: RUNNER_TOKEN is required" >&2
|
|
9
|
-
exit 1
|
|
10
|
-
fi
|
|
11
|
-
|
|
12
|
-
if [ -z "$PROJECT_DIR" ]; then
|
|
13
|
-
echo "ERROR: PROJECT_DIR is required" >&2
|
|
14
|
-
exit 1
|
|
15
|
-
fi
|
|
16
|
-
|
|
17
|
-
sed \
|
|
18
|
-
-e "s|__RUNNER_NAME__|${RUNNER_NAME}|g" \
|
|
19
|
-
-e "s|__RUNNER_TOKEN__|${RUNNER_TOKEN}|g" \
|
|
20
|
-
-e "s|__PROJECT_DIR__|${PROJECT_DIR}|g" \
|
|
21
|
-
-e "s|__RUNNER_TAGS__|${RUNNER_TAGS}|g" \
|
|
22
|
-
-e "s|__GITLAB_URL__|${GITLAB_URL}|g" \
|
|
23
|
-
-e "s|__GITLAB_VERSION__|${GITLAB_VERSION}|g" \
|
|
24
|
-
-e "s|__HELPER_VERSION__|${HELPER_VERSION}|g" \
|
|
25
|
-
"$TEMPLATE" > "$CONFIG"
|
|
26
|
-
|
|
27
|
-
exec gitlab-runner run
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
set -e
|
|
3
|
-
cd "$(dirname "$0")"
|
|
4
|
-
|
|
5
|
-
# Проверка наличия .env файла
|
|
6
|
-
if [ ! -f .env ]; then
|
|
7
|
-
echo "ERROR: .env file not found in $(pwd)"
|
|
8
|
-
echo "Copy .env.example to .env and fill in the values:"
|
|
9
|
-
echo " cp .env.example .env"
|
|
10
|
-
exit 1
|
|
11
|
-
fi
|
|
12
|
-
|
|
13
|
-
# Загружаем .env в текущий shell
|
|
14
|
-
set -a
|
|
15
|
-
. .env
|
|
16
|
-
set +a
|
|
17
|
-
|
|
18
|
-
# Проверка обязательных переменных
|
|
19
|
-
required_vars=(
|
|
20
|
-
"GITLAB_URL"
|
|
21
|
-
"RUNNER_NAME"
|
|
22
|
-
"RUNNER_TOKEN"
|
|
23
|
-
)
|
|
24
|
-
missing=()
|
|
25
|
-
for var in "${required_vars[@]}"; do
|
|
26
|
-
if [ -z "${!var}" ]; then
|
|
27
|
-
missing+=("$var")
|
|
28
|
-
fi
|
|
29
|
-
done
|
|
30
|
-
if [ ${#missing[@]} -gt 0 ]; then
|
|
31
|
-
echo "ERROR: Required variables not set in .env:"
|
|
32
|
-
for var in "${missing[@]}"; do
|
|
33
|
-
echo " - $var"
|
|
34
|
-
done
|
|
35
|
-
exit 1
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
# PROJECT_DIR: если не задан — два уровня вверх (корень проекта)
|
|
39
|
-
if [ -z "$PROJECT_DIR" ]; then
|
|
40
|
-
PROJECT_DIR="../.."
|
|
41
|
-
fi
|
|
42
|
-
|
|
43
|
-
# Резолвим в абсолютный путь
|
|
44
|
-
PROJECT_DIR=$(cd "$PROJECT_DIR" && pwd)
|
|
45
|
-
export PROJECT_DIR
|
|
46
|
-
|
|
47
|
-
exec docker compose up -d "$@"
|