fa-mcp-sdk 0.4.66 → 0.4.68
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/deploy-mcp/SKILL.md +5 -1
- package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/02-1-tools-and-api.md +188 -45
- package/cli-template/FA-MCP-SDK-DOC/03-configuration.md +152 -0
- package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +3 -1
- package/cli-template/package.json +1 -1
- package/package.json +1 -1
- package/scripts/update-doc.js +31 -13
|
@@ -260,7 +260,11 @@ Follow the plan. For each tool/resource/prompt:
|
|
|
260
260
|
leave demo code in the final build.
|
|
261
261
|
2. Add new config keys to `config/default.yaml` (and matching env mappings in
|
|
262
262
|
`config/custom-environment-variables.yaml` when appropriate). Mirror structural changes
|
|
263
|
-
in `config/_local.yaml`.
|
|
263
|
+
in `config/_local.yaml`. **If the feature talks to any third-party / external service
|
|
264
|
+
(REST API, legacy system, partner endpoint), put its connection attributes — `host`,
|
|
265
|
+
`port`, `protocol`, `token`, credentials, custom fields — under the `accessPoints` block,
|
|
266
|
+
not ad-hoc sections. See `FA-MCP-SDK-DOC/03-configuration.md` → "Access Points" for the
|
|
267
|
+
YAML shape and access pattern.**
|
|
264
268
|
3. Update `tests/mcp/test-cases.js` with real cases.
|
|
265
269
|
4. `yarn cb` after each meaningful change; don't accumulate type errors.
|
|
266
270
|
|
|
@@ -15,7 +15,7 @@ npm install fa-mcp-sdk
|
|
|
15
15
|
| [01-getting-started](01-getting-started.md) | `initMcpServer()`, `McpServerData`, `IPromptData`, `IResourceData`, `AppConfig` | Starting new project |
|
|
16
16
|
| [02-1-tools-and-api](02-1-tools-and-api.md) | Tool definitions, `toolHandler`, outbound webhooks, REST API with tsoa, OpenAPI/Swagger | Creating tools, REST endpoints, webhook callbacks |
|
|
17
17
|
| [02-2-prompts-and-resources](02-2-prompts-and-resources.md) | Standard/custom prompts, resources, `requireAuth` | Configuring prompts/resources |
|
|
18
|
-
| [03-configuration](03-configuration.md) | `appConfig`, YAML config, cache, PostgreSQL | Server configuration, DB |
|
|
18
|
+
| [03-configuration](03-configuration.md) | `appConfig`, YAML config, access points for external services, cache, PostgreSQL | Server configuration, external services, DB |
|
|
19
19
|
| [04-authentication](04-authentication.md) | JWT, Basic auth, server tokens, `createAuthMW()`, Token Generator, CLI Token Generator, JWT Generation API | Authentication setup |
|
|
20
20
|
| [05-ad-authorization](05-ad-authorization.md) | AD group authorization at HTTP/tool levels | AD group restrictions |
|
|
21
21
|
| [06-utilities](06-utilities.md) | `ServerError`, `normalizeHeaders`, logging, Consul, graceful shutdown | Error handling, utilities |
|
|
@@ -62,65 +62,208 @@ const clientIP = headers?.['x-real-ip'] || headers?.['x-forwarded-for'];
|
|
|
62
62
|
`IToolHandlerParams` includes `ITransportContext` fields (`transport`, `headers`, `payload`).
|
|
63
63
|
See [ITransportContext](./02-2-prompts-and-resources.md#itransportcontext).
|
|
64
64
|
|
|
65
|
-
### Outbound Webhooks
|
|
65
|
+
### Outbound Webhooks (`x-web-hook`)
|
|
66
66
|
|
|
67
|
-
The SDK does not ship a built-in webhook — it
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
`callWebHook` + dispatch block).
|
|
67
|
+
Handler-level pattern. The SDK does **not** ship a built-in webhook dispatcher — it exposes
|
|
68
|
+
everything you need (`params.headers`, `appConfig`, `logger`) and leaves the policy to the project.
|
|
69
|
+
This section is the **canonical recipe**: implement it as written so every fa-mcp-sdk-based MCP
|
|
70
|
+
server behaves the same way for clients and downstream collectors.
|
|
72
71
|
|
|
73
|
-
**
|
|
72
|
+
**What it is:** after every tool invocation the server can `POST` the tool result to an external
|
|
73
|
+
URL. Useful for audit trails, real-time dashboards, chaining MCP calls into CI/automation pipelines.
|
|
74
|
+
Opt-in per request (via header) and optionally per tool (via the response object). A failing webhook
|
|
75
|
+
**must never** fail the tool call.
|
|
74
76
|
|
|
75
|
-
|
|
77
|
+
#### Contract (stable across all MCPs)
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
usedHttpHeaders: [
|
|
79
|
-
{ name: 'x-web-hook', description: 'URL to POST the tool result to.', isOptional: true },
|
|
80
|
-
],
|
|
81
|
-
```
|
|
79
|
+
**Inbound — precedence:**
|
|
82
80
|
|
|
83
|
-
|
|
81
|
+
| Source | Form | Precedence |
|
|
82
|
+
|---------------------|---------------------------------------------------------|------------|
|
|
83
|
+
| Per-tool override | `IToolResponse.hook: string` returned by the handler | wins |
|
|
84
|
+
| Per-request header | `x-web-hook: <http(s) URL>` | fallback |
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
import axios from 'axios';
|
|
87
|
-
import { appConfig, logger, toStr, IToolHandlerParams } from 'fa-mcp-sdk';
|
|
86
|
+
If neither is present, no webhook fires.
|
|
88
87
|
|
|
89
|
-
|
|
88
|
+
**Outbound request:**
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
axios.post(url, { mcpName: appConfig.name, tool, user, response }, { timeout: 10_000 })
|
|
94
|
-
.catch((err) => logger.warn(`Web-hook POST ${url} failed: ${toStr(err?.message || err)}`));
|
|
95
|
-
};
|
|
90
|
+
- Method: `POST`, `Content-Type: application/json`, timeout ≤ 10 000 ms
|
|
91
|
+
- Body:
|
|
96
92
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
```json
|
|
94
|
+
{
|
|
95
|
+
"mcpName": "<appConfig.name>",
|
|
96
|
+
"tool": "<tool_name>",
|
|
97
|
+
"user": "<caller-id-or-omitted>",
|
|
98
|
+
"response": { "...": "tool's full JSON result" }
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
| Field | Description |
|
|
103
|
+
|------------|------------------------------------------------------------------------------|
|
|
104
|
+
| `mcpName` | `appConfig.name` — identifies which MCP sent the callback |
|
|
105
|
+
| `tool` | Name of the invoked tool |
|
|
106
|
+
| `user` | Best-effort caller identity (see *User resolution*); **omit** if unresolved |
|
|
107
|
+
| `response` | Full JSON returned by the tool handler (same payload sent to the client) |
|
|
108
|
+
|
|
109
|
+
Do **not** add ad-hoc fields on a per-project basis without versioning the body — downstream
|
|
110
|
+
collectors rely on this exact shape.
|
|
111
|
+
|
|
112
|
+
#### Implementation recipe
|
|
113
|
+
|
|
114
|
+
**1. Declare the header** so `use://http-headers`, Agent Tester, and tool-call introspection
|
|
115
|
+
advertise it:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// src/start.ts
|
|
119
|
+
usedHttpHeaders.push({
|
|
120
|
+
name: 'x-web-hook',
|
|
121
|
+
description:
|
|
122
|
+
'Optional URL called via POST after each tool invocation. '
|
|
123
|
+
+ 'Body: { mcpName, tool, user, response }. Fire-and-forget; failures are logged only.',
|
|
124
|
+
isOptional: true,
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**2. Add `hook?` to the internal tool-response type** (lets a handler override the URL per tool):
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
// src/_types_/tool.ts
|
|
132
|
+
export interface IToolResponse {
|
|
133
|
+
text: string;
|
|
134
|
+
json: Record<string, any>;
|
|
135
|
+
hook?: string; // per-tool URL override; takes precedence over x-web-hook header
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**3. Dispatcher — fire-and-forget, never throws:**
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// src/tools/tools-manager.ts
|
|
143
|
+
import axios from 'axios';
|
|
144
|
+
import { appConfig, logger as lgr, toStr } from 'fa-mcp-sdk';
|
|
145
|
+
|
|
146
|
+
const logger = lgr.getSubLogger({ name: 'tools' });
|
|
147
|
+
const URL_REGEX = /^https?:\/\/[^\s]+$/i;
|
|
148
|
+
|
|
149
|
+
const callWebHook = (
|
|
150
|
+
url: string,
|
|
151
|
+
toolName: string,
|
|
152
|
+
json: Record<string, any>,
|
|
153
|
+
user?: string,
|
|
154
|
+
): void => {
|
|
155
|
+
if (!URL_REGEX.test(url)) { return; } // silently drop garbage URLs
|
|
156
|
+
const body = { mcpName: appConfig.name, tool: toolName, response: json, user };
|
|
157
|
+
axios.post(url, body, { timeout: 10_000 })
|
|
158
|
+
.catch((err) => logger.warn(`Web-hook POST ${url} failed: ${toStr(err?.message || err)}`));
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Rules:
|
|
105
163
|
|
|
106
|
-
|
|
107
|
-
|
|
164
|
+
- **No `await`.** The webhook must not delay the MCP response.
|
|
165
|
+
- **No re-throws.** A 5xx, timeout, or DNS failure is a `warn` log, nothing more.
|
|
166
|
+
- **URL allow-list.** At minimum, require `http(s)://`. Add an internal-net allow-list via config
|
|
167
|
+
(e.g. `webhook.allowedHosts`) if the threat model requires it (see *Security*).
|
|
108
168
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
```
|
|
169
|
+
**4. Wire it into the tool-call entry point** — dispatch after the handler resolves and before
|
|
170
|
+
the result is returned:
|
|
112
171
|
|
|
113
|
-
|
|
172
|
+
```typescript
|
|
173
|
+
export const handleToolCall = async (params: IToolHandlerParams): Promise<any> => {
|
|
174
|
+
const { name: toolName, arguments: args, headers: mcpRequestHeaders = {} } = params;
|
|
175
|
+
|
|
176
|
+
const tool = (await getTools(mcpRequestHeaders)).get(toolName);
|
|
177
|
+
if (!tool?.handler) { throw new ToolExecutionError(toolName, `Unknown tool: ${toolName}`); }
|
|
178
|
+
|
|
179
|
+
const ctx: ToolContext = {
|
|
180
|
+
httpClient: createHttpClient(mcpRequestHeaders),
|
|
181
|
+
logger: logger.getSubLogger({ name: toolName }),
|
|
182
|
+
mcpRequestHeaders,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const toolResponse: IToolResponse = await tool.handler(args, ctx);
|
|
186
|
+
|
|
187
|
+
// ─── webhook dispatch (fire-and-forget) ─────────────────────────────────────
|
|
188
|
+
const hookUrl = (toolResponse?.hook || mcpRequestHeaders['x-web-hook'] || '').trim();
|
|
189
|
+
if (hookUrl) {
|
|
190
|
+
const syncUser = resolveActualUser(mcpRequestHeaders); // see step 5
|
|
191
|
+
if (syncUser) {
|
|
192
|
+
callWebHook(hookUrl, toolName, toolResponse.json, syncUser);
|
|
193
|
+
} else {
|
|
194
|
+
// Async user resolution — still fire-and-forget; do not block the tool response.
|
|
195
|
+
getCachedSelfUser(ctx.httpClient, mcpRequestHeaders)
|
|
196
|
+
.then((u) => callWebHook(hookUrl, toolName, toolResponse.json, u));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
return formatToolResult(toolResponse);
|
|
202
|
+
};
|
|
203
|
+
```
|
|
114
204
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
205
|
+
**5. User resolution — best-effort, two-step.** The `user` field is what makes the webhook useful
|
|
206
|
+
for audit. Resolve carefully, but never let resolution fail the call.
|
|
207
|
+
|
|
208
|
+
- **Step A — Sync (preferred):** derive from headers / JWT payload / config without I/O
|
|
209
|
+
(e.g. JWT `payload.user`, a custom `x-actual-user` header your auth layer stamps, etc.).
|
|
210
|
+
- **Step B — Async fallback (only when sync returns nothing):** call the upstream "who am I"
|
|
211
|
+
endpoint with the same auth, **cache the result** (recommended TTL: 1 h, key by hashed
|
|
212
|
+
`Authorization`), and dedupe in-flight requests (thundering-herd protection).
|
|
213
|
+
- If both steps fail → **omit** the `user` field. Never invent a placeholder like `"unknown"`.
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
export function resolveActualUser (headers: Record<string, string>): string | undefined { /* … */ }
|
|
217
|
+
|
|
218
|
+
export const getCachedSelfUser = async (
|
|
219
|
+
httpClient: AxiosInstance,
|
|
220
|
+
headers: Record<string, string>,
|
|
221
|
+
): Promise<string | undefined> => { /* GET /me, cache by hashed Authorization, dedupe */ };
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
#### Per-tool override — when to use
|
|
225
|
+
|
|
226
|
+
A handler may force a specific webhook URL:
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
return { text, json, hook: 'https://collector.internal/special' };
|
|
230
|
+
```
|
|
121
231
|
|
|
122
|
-
|
|
123
|
-
|
|
232
|
+
Use sparingly. Legitimate cases:
|
|
233
|
+
|
|
234
|
+
- a long-running tool whose result feeds a fixed pipeline regardless of the client;
|
|
235
|
+
- a tool that should **never** webhook (e.g. read of a secret) — return `hook: ''` only if the
|
|
236
|
+
dispatcher treats empty string as "skip even if header is set". With the snippet above this works
|
|
237
|
+
naturally because `(toolResponse?.hook || header)` short-circuits on any truthy `hook`; to force
|
|
238
|
+
skip, have the handler strip the header from `ctx` or short-circuit `hookUrl` explicitly.
|
|
239
|
+
|
|
240
|
+
If neither applies, do not set `hook` — let the client decide.
|
|
241
|
+
|
|
242
|
+
#### Security
|
|
243
|
+
|
|
244
|
+
- **URL validation** — reject anything that does not match `http(s)://…`. For public-facing MCPs,
|
|
245
|
+
restrict to a configured allow-list (`webhook.allowedHosts` in `config/default.yaml`).
|
|
246
|
+
- **SSRF surface** — the webhook is a server-side `POST` to a client-supplied URL. Acceptable for
|
|
247
|
+
trusted MCP clients; not acceptable open on the internet without an allow-list.
|
|
248
|
+
- **No secrets in the body** — `response` is the same JSON the client already received. Do **not**
|
|
249
|
+
add credentials, raw tokens, or PII not present in the response.
|
|
250
|
+
- **No retries** — duplicate POSTs to a flaky collector are worse than a missed event. If the
|
|
251
|
+
collector needs guarantees, let it poll.
|
|
252
|
+
- **Logging** — log `tool`, target host, and outcome at `warn`/`debug`; **never** log the full body
|
|
253
|
+
at `info` level (audit log noise + potential PII).
|
|
254
|
+
|
|
255
|
+
#### Testing checklist
|
|
256
|
+
|
|
257
|
+
- [ ] Header declared in `usedHttpHeaders` and visible at `/use://http-headers`.
|
|
258
|
+
- [ ] Tool call **without** `x-web-hook` → no outbound POST.
|
|
259
|
+
- [ ] Tool call **with** valid `x-web-hook` → exactly one POST, body matches the contract above.
|
|
260
|
+
- [ ] Collector returns 500 → tool response still succeeds; one `warn` line in the log.
|
|
261
|
+
- [ ] Collector hangs → tool response returns within normal latency; POST aborts at 10 s.
|
|
262
|
+
- [ ] Malformed URL (`javascript:…`, missing scheme) → no POST, no error to client.
|
|
263
|
+
- [ ] Per-tool `hook` set → wins over the header.
|
|
264
|
+
- [ ] Sync user resolution hits → `user` populated immediately, no extra HTTP call.
|
|
265
|
+
- [ ] Sync empty, async succeeds → POST fires after `/me` resolves; tool response was not delayed.
|
|
266
|
+
- [ ] Both user paths fail → POST fires with `user` **field omitted** (not `null`, not `"unknown"`).
|
|
124
267
|
|
|
125
268
|
|
|
126
269
|
## REST API Endpoints
|
|
@@ -183,6 +183,158 @@ webServer:
|
|
|
183
183
|
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
## Access Points
|
|
187
|
+
|
|
188
|
+
If your MCP server talks to third-party / external services (REST APIs, legacy systems, partner endpoints, etc.),
|
|
189
|
+
declare their connection attributes (`host`, `port`, `protocol`, `token`, credentials, custom fields) under the
|
|
190
|
+
top-level `accessPoints` block in the config — **not** scattered through code or ad-hoc config sections. Benefits:
|
|
191
|
+
|
|
192
|
+
- Single registry of outbound dependencies visible in diagnostics and admin pages.
|
|
193
|
+
- Automatic `host`/`port` resolution via Consul for services registered there.
|
|
194
|
+
- Uniform access pattern (`appConfig.accessPoints.<alias>`) across all tools and modules.
|
|
195
|
+
- Runtime updates — the SDK periodically refreshes dynamic access points from Consul without restarting the server.
|
|
196
|
+
|
|
197
|
+
The SDK automatically wraps `appConfig.accessPoints` in an `AccessPoints` instance on startup and starts the Consul
|
|
198
|
+
updater — **do not call `new AccessPoints(...)` or `accessPointUpdater.start()` manually**.
|
|
199
|
+
|
|
200
|
+
### Declaring Access Points
|
|
201
|
+
|
|
202
|
+
```yaml
|
|
203
|
+
accessPoints:
|
|
204
|
+
# Dynamic AP — host/port resolved from Consul
|
|
205
|
+
wso2siAPI:
|
|
206
|
+
title: 'WSO2 SI API'
|
|
207
|
+
consulServiceName: 'dev01-wso2si-d2'
|
|
208
|
+
host: null # filled in from Consul
|
|
209
|
+
port: 9443 # fallback; also used when Consul meta specifies a different port
|
|
210
|
+
protocol: 'https'
|
|
211
|
+
user: 'admin'
|
|
212
|
+
pass: '***'
|
|
213
|
+
myProp: 'anyValue' # any custom field is preserved and available at runtime
|
|
214
|
+
|
|
215
|
+
# Static AP — Consul is NOT used
|
|
216
|
+
externalAPI:
|
|
217
|
+
noConsul: true
|
|
218
|
+
host: 'api.partner.com'
|
|
219
|
+
port: 443
|
|
220
|
+
protocol: 'https'
|
|
221
|
+
token: '***'
|
|
222
|
+
timeoutMs: 5000
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Using Access Points in Code
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { appConfig } from 'fa-mcp-sdk';
|
|
229
|
+
|
|
230
|
+
// Direct access — always works, for dynamic and static APs alike
|
|
231
|
+
const ap = appConfig.accessPoints.wso2siAPI;
|
|
232
|
+
const url = `${ap.protocol}://${ap.host}:${ap.port}`;
|
|
233
|
+
const token = ap.token; // custom fields available
|
|
234
|
+
const custom = ap.myProp;
|
|
235
|
+
|
|
236
|
+
// "Clean" copy without service fields
|
|
237
|
+
const ap2 = appConfig.accessPoints.getAP('wso2siAPI');
|
|
238
|
+
|
|
239
|
+
// All access points at once
|
|
240
|
+
const all = appConfig.accessPoints.get();
|
|
241
|
+
|
|
242
|
+
// For dynamic APs — wait until host/port are resolved from Consul (first run)
|
|
243
|
+
await ap.waitForHostPortUpdated(5000);
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
**Strict typing for custom fields:**
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import type { IAccessPoint } from 'fa-consul';
|
|
250
|
+
|
|
251
|
+
interface IWso2AP extends IAccessPoint {
|
|
252
|
+
user: string;
|
|
253
|
+
pass: string;
|
|
254
|
+
myProp: string;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const ap = appConfig.accessPoints.wso2siAPI as IWso2AP;
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Access Point Properties
|
|
261
|
+
|
|
262
|
+
**User-defined (configured in YAML):**
|
|
263
|
+
|
|
264
|
+
| Property | Required | Purpose |
|
|
265
|
+
|----------------------------------|-----------------|---------------------------------------------------------------------------------------|
|
|
266
|
+
| `consulServiceName` | yes for dynamic | Consul service name used to resolve `host`/`port` |
|
|
267
|
+
| `host` | — | IP/hostname. Dynamic: usually `null`, filled from Consul. Static (`noConsul`): manual |
|
|
268
|
+
| `port` | — | TCP port. Coerced to `Number` or `null`. Dynamic: from Consul (or `meta.port`) |
|
|
269
|
+
| `protocol` | — | `http` or `https`. Anything other than `https?` is coerced to `http`, lowercased |
|
|
270
|
+
| `title` | — | Human-readable name (defaults to the AP key) |
|
|
271
|
+
| `noConsul` | — | `true` → static AP: Consul is not polled, `consulServiceName` is not required |
|
|
272
|
+
| `retrieveProps` | — | `(host, meta) => ({host, port})`. Custom extractor for Consul response |
|
|
273
|
+
| `updateIntervalIfSuccessMillis` | — | Interval between successful Consul polls for this AP (default 2 min) |
|
|
274
|
+
| `user`, `pass`, `token`, any key | — | Application fields — stored as-is and available at runtime |
|
|
275
|
+
|
|
276
|
+
**Service fields (added automatically by the SDK for dynamic APs):**
|
|
277
|
+
|
|
278
|
+
| Property | Purpose |
|
|
279
|
+
|------------------------------|-------------------------------------------------------------------------|
|
|
280
|
+
| `id` | The AP key from the config |
|
|
281
|
+
| `isAP` | Marker for a dynamic AP; absent on `noConsul` APs |
|
|
282
|
+
| `meta` | Filled from `Service.Meta` of the Consul service on successful poll |
|
|
283
|
+
| `isReachable` | `true` if the last Consul poll returned data |
|
|
284
|
+
| `lastSuccessUpdate` | Timestamp of the last successful update |
|
|
285
|
+
| `idHostPortUpdated` | `true` once `host` + `port` have been populated at least once |
|
|
286
|
+
| `setProps(data)` | Method for externally updating AP fields |
|
|
287
|
+
| `waitForHostPortUpdated(ms)` | Promise that resolves when `host`/`port` have been populated |
|
|
288
|
+
| `getChanges()` | Returns `[propName, oldValue, newValue][]` for the last `setProps` call |
|
|
289
|
+
|
|
290
|
+
### `noConsul` Access Points
|
|
291
|
+
|
|
292
|
+
Setting `noConsul: true` makes the access point **static** — its address is not resolved through Consul. Typical use
|
|
293
|
+
cases: partner APIs, legacy systems, or services with fixed addresses that cannot (or should not) be registered in
|
|
294
|
+
Consul.
|
|
295
|
+
|
|
296
|
+
Differences from a dynamic AP:
|
|
297
|
+
|
|
298
|
+
- `consulServiceName` is not required.
|
|
299
|
+
- The AP object is stored **as-is** — no normalization of `port`/`protocol`, no service fields (`isAP`, `setProps`,
|
|
300
|
+
`waitForHostPortUpdated`, etc.) are added.
|
|
301
|
+
- The AP is excluded from Consul polling; `host`/`port` are never overwritten.
|
|
302
|
+
- `getAP('key')` and `get()` do **not** return static APs by default (they filter on `isAP`). Pass `andNotIsAP = true`
|
|
303
|
+
to include them, or use direct access — `appConfig.accessPoints.externalAPI` — which always works.
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
appConfig.accessPoints.getAP('externalAPI', true); // include static AP in lookup
|
|
307
|
+
appConfig.accessPoints.externalAPI; // direct access always works
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Custom Fields
|
|
311
|
+
|
|
312
|
+
Any additional property on an AP (`apiKey`, `timeoutMs`, `headers: {...}`, etc.) is preserved verbatim and accessible at
|
|
313
|
+
runtime:
|
|
314
|
+
|
|
315
|
+
- On creation, all fields from the config are copied onto the AP object.
|
|
316
|
+
- Periodic Consul updates only refresh `host`/`port` (and optionally `meta`) — other properties are **never
|
|
317
|
+
overwritten**.
|
|
318
|
+
- `get()` / `getAP()` copy all enumerable properties except `undefined` and functions.
|
|
319
|
+
- Nested objects are copied shallowly — if a custom field is an object, its inner references are shared with the
|
|
320
|
+
original config.
|
|
321
|
+
- Only `port` (coerced to `Number`) and `protocol` (coerced to `http`/`https`) are normalized; all other fields are
|
|
322
|
+
left untouched.
|
|
323
|
+
|
|
324
|
+
### Subscribing to Updates
|
|
325
|
+
|
|
326
|
+
When a dynamic AP is refreshed from Consul, events are emitted on the SDK's `eventEmitter`
|
|
327
|
+
(see "Event System" in `06-utilities.md`):
|
|
328
|
+
|
|
329
|
+
```typescript
|
|
330
|
+
import { eventEmitter } from 'fa-mcp-sdk';
|
|
331
|
+
|
|
332
|
+
eventEmitter.on('access-point-updated', ({ accessPoint, changes }) => {
|
|
333
|
+
// changes: [propName, oldValue, newValue][]
|
|
334
|
+
});
|
|
335
|
+
eventEmitter.on('access-points-updated', () => { /* any AP was updated this cycle */ });
|
|
336
|
+
```
|
|
337
|
+
|
|
186
338
|
## Cache
|
|
187
339
|
|
|
188
340
|
```typescript
|
|
@@ -143,7 +143,9 @@ import { getConsulAPI, accessPointUpdater, deregisterServiceFromConsul } from 'f
|
|
|
143
143
|
const consul = await getConsulAPI();
|
|
144
144
|
const services = await consul.catalog.service.list();
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
// accessPointUpdater is started/stopped by the SDK automatically — see 03-configuration.md → "Access Points".
|
|
147
|
+
// The start()/stop() hooks below are exposed only for tests and diagnostics.
|
|
148
|
+
accessPointUpdater.start();
|
|
147
149
|
accessPointUpdater.stop();
|
|
148
150
|
|
|
149
151
|
await deregisterServiceFromConsul();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fa-mcp-sdk",
|
|
3
3
|
"productName": "FA MCP SDK",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.68",
|
|
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",
|
package/scripts/update-doc.js
CHANGED
|
@@ -1,18 +1,36 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { cpSync, existsSync, rmSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'fs';
|
|
3
|
+
import { basename, dirname, join } from 'path';
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
5
|
+
const templateDir = join(process.cwd(), './node_modules/fa-mcp-sdk/cli-template');
|
|
6
|
+
const cwd = process.cwd();
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
const targets = [
|
|
9
|
+
{ name: 'FA-MCP-SDK-DOC', src: join(templateDir, 'FA-MCP-SDK-DOC'), dest: join(cwd, 'FA-MCP-SDK-DOC') },
|
|
10
|
+
{ name: '.claude', src: join(templateDir, '.claude'), dest: join(cwd, '.claude'), preserve: ['settings.json'] },
|
|
11
|
+
];
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
for (const { name, src, dest, preserve = [] } of targets) {
|
|
14
|
+
if (!existsSync(src)) {
|
|
15
|
+
console.error('Source not found:', src);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
const saved = {};
|
|
19
|
+
for (const file of preserve) {
|
|
20
|
+
const p = join(dest, file);
|
|
21
|
+
if (existsSync(p)) saved[file] = readFileSync(p);
|
|
22
|
+
}
|
|
23
|
+
if (existsSync(dest)) {
|
|
24
|
+
rmSync(dest, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
cpSync(src, dest, {
|
|
27
|
+
recursive: true,
|
|
28
|
+
filter: (srcPath) => !preserve.includes(basename(srcPath)),
|
|
29
|
+
});
|
|
30
|
+
for (const [file, content] of Object.entries(saved)) {
|
|
31
|
+
const p = join(dest, file);
|
|
32
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
33
|
+
writeFileSync(p, content);
|
|
34
|
+
}
|
|
35
|
+
console.log(`${name} updated`);
|
|
15
36
|
}
|
|
16
|
-
|
|
17
|
-
cpSync(src, dest, { recursive: true });
|
|
18
|
-
console.log('FA-MCP-SDK-DOC updated');
|