afpnews-mcp-server 2.0.0 → 2.0.1
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/.serena/project.yml +14 -0
- package/README.md +5 -1
- package/bun.lock +17 -13
- package/package.json +10 -7
- package/src/__tests__/create-server.test.ts +44 -46
- package/src/__tests__/format-media.test.ts +8 -8
- package/src/__tests__/format.test.ts +3 -3
- package/src/__tests__/get-media-handler.test.ts +120 -0
- package/src/__tests__/get-media.test.ts +1 -1
- package/src/__tests__/list-facets.test.ts +52 -0
- package/src/__tests__/server.test.ts +24 -13
- package/src/__tests__/tool-defaults.test.ts +53 -0
- package/src/__tests__/types-media.test.ts +1 -1
- package/src/tools/find-similar.ts +2 -2
- package/src/tools/get-article.ts +1 -1
- package/src/tools/get-media.ts +7 -5
- package/src/tools/index.ts +1 -1
- package/src/tools/list-facets.ts +19 -14
- package/src/tools/search-articles.ts +20 -10
- package/src/tools/search-media.ts +22 -54
- package/src/tools/shared.ts +2 -2
- package/src/utils/format-media.ts +67 -16
- package/src/utils/format.ts +74 -60
- package/src/utils/types.ts +1 -1
- package/tsconfig.json +1 -1
- package/tsconfig.test.json +9 -0
package/.serena/project.yml
CHANGED
|
@@ -133,3 +133,17 @@ read_only_memory_patterns: []
|
|
|
133
133
|
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
|
134
134
|
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
|
135
135
|
line_ending:
|
|
136
|
+
|
|
137
|
+
# list of regex patterns for memories to completely ignore.
|
|
138
|
+
# Matching memories will not appear in list_memories or activate_project output
|
|
139
|
+
# and cannot be accessed via read_memory or write_memory.
|
|
140
|
+
# To access ignored memory files, use the read_file tool on the raw file path.
|
|
141
|
+
# Extends the list from the global configuration, merging the two lists.
|
|
142
|
+
# Example: ["_archive/.*", "_episodes/.*"]
|
|
143
|
+
ignored_memory_patterns: []
|
|
144
|
+
|
|
145
|
+
# advanced configuration option allowing to configure language server-specific options.
|
|
146
|
+
# Maps the language key to the options.
|
|
147
|
+
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
|
148
|
+
# No documentation on options means no options are available.
|
|
149
|
+
ls_specific_settings: {}
|
package/README.md
CHANGED
|
@@ -138,7 +138,11 @@ The `afp_search_articles` tool supports presets that apply predefined filters:
|
|
|
138
138
|
|
|
139
139
|
### Full text
|
|
140
140
|
|
|
141
|
-
By default, `afp_search_articles` returns excerpts (first
|
|
141
|
+
By default, `afp_search_articles` returns excerpts (first 2 paragraphs). Set `fullText: true` to get the complete article body. Presets default to full text.
|
|
142
|
+
|
|
143
|
+
### Pagination
|
|
144
|
+
|
|
145
|
+
Use `offset` to paginate through results (e.g. `offset: 10` to skip the first 10). For large chronological scans, prefer narrowing `dateFrom`/`dateTo` ranges in `facets` over high offsets. Keep `size` small (10–20) for best performance.
|
|
142
146
|
|
|
143
147
|
## Prompts
|
|
144
148
|
|
package/bun.lock
CHANGED
|
@@ -5,15 +5,19 @@
|
|
|
5
5
|
"": {
|
|
6
6
|
"name": "afpnews-mcp-server",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
9
|
-
"afpnews-api": "^2.4.0",
|
|
8
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
10
9
|
"elysia": "^1.4.28",
|
|
11
10
|
"elysia-rate-limit": "^4.5.1",
|
|
12
|
-
"jose": "^6.2.
|
|
13
|
-
"zod": "^4.3.6",
|
|
11
|
+
"jose": "^6.2.2",
|
|
14
12
|
},
|
|
15
13
|
"devDependencies": {
|
|
16
|
-
"@types/node": "^25.5.
|
|
14
|
+
"@types/node": "^25.5.2",
|
|
15
|
+
"afpnews-api": "^2.4.1",
|
|
16
|
+
"zod": "^4.3.6",
|
|
17
|
+
},
|
|
18
|
+
"peerDependencies": {
|
|
19
|
+
"afpnews-api": "^2.4.0",
|
|
20
|
+
"zod": "^4.3.6",
|
|
17
21
|
},
|
|
18
22
|
},
|
|
19
23
|
},
|
|
@@ -24,7 +28,7 @@
|
|
|
24
28
|
|
|
25
29
|
"@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "4.11.9" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="],
|
|
26
30
|
|
|
27
|
-
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.
|
|
31
|
+
"@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="],
|
|
28
32
|
|
|
29
33
|
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
|
30
34
|
|
|
@@ -38,13 +42,13 @@
|
|
|
38
42
|
|
|
39
43
|
"@types/nearley": ["@types/nearley@2.11.5", "", {}, "sha512-dM7TrN0bVxGGXTYGx4YhGear8ysLO5SOuouAWM9oltjQ3m9oYa13qi8Z1DJp5zxVMPukvQdsrnZmgzpeuTSEQA=="],
|
|
40
44
|
|
|
41
|
-
"@types/node": ["@types/node@25.5.
|
|
45
|
+
"@types/node": ["@types/node@25.5.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg=="],
|
|
42
46
|
|
|
43
47
|
"@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="],
|
|
44
48
|
|
|
45
49
|
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "3.0.1", "negotiator": "1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
|
46
50
|
|
|
47
|
-
"afpnews-api": ["afpnews-api@2.4.
|
|
51
|
+
"afpnews-api": ["afpnews-api@2.4.1", "", { "dependencies": { "@types/btoa-lite": "^1.0.2", "@types/moo": "^0.5.10", "@types/nearley": "^2.11.5", "@types/node": "^25.5.0", "@types/statuses": "^2.0.6", "btoa-lite": "^1.0.0", "events": "^3.3.0", "moo": "^0.5.3", "nearley": "^2.20.1", "statuses": "^2.0.2", "zod": "^4.3.6" } }, "sha512-A/1vZeq/qqLph11XYZbXqhs0qUSQ9zi/4YK2a+XVpn5iF6q1VXWABW19lL0X2mZM7RsdsEUW+rMIhDmHbdFsBw=="],
|
|
48
52
|
|
|
49
53
|
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "3.1.3", "fast-uri": "3.1.0", "json-schema-traverse": "1.0.0", "require-from-string": "2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
|
50
54
|
|
|
@@ -156,7 +160,7 @@
|
|
|
156
160
|
|
|
157
161
|
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
|
158
162
|
|
|
159
|
-
"jose": ["jose@6.2.
|
|
163
|
+
"jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="],
|
|
160
164
|
|
|
161
165
|
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
|
162
166
|
|
|
@@ -174,7 +178,7 @@
|
|
|
174
178
|
|
|
175
179
|
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
|
|
176
180
|
|
|
177
|
-
"moo": ["moo@0.5.
|
|
181
|
+
"moo": ["moo@0.5.3", "", {}, "sha512-m2fmM2dDm7GZQsY7KK2cme8agi+AAljILjQnof7p1ZMDe6dQ4bdnSMx0cPppudoeNv5hEFQirN6u+O4fDE0IWA=="],
|
|
178
182
|
|
|
179
183
|
"ms": ["ms@2.1.2", "", {}, "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="],
|
|
180
184
|
|
|
@@ -270,7 +274,7 @@
|
|
|
270
274
|
|
|
271
275
|
"@tokenizer/inflate/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
272
276
|
|
|
273
|
-
"afpnews-api/@types/node": ["@types/node@25.
|
|
277
|
+
"afpnews-api/@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
|
274
278
|
|
|
275
279
|
"body-parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
276
280
|
|
|
@@ -280,6 +284,8 @@
|
|
|
280
284
|
|
|
281
285
|
"finalhandler/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
282
286
|
|
|
287
|
+
"nearley/moo": ["moo@0.5.2", "", {}, "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q=="],
|
|
288
|
+
|
|
283
289
|
"router/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
284
290
|
|
|
285
291
|
"send/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
|
@@ -288,8 +294,6 @@
|
|
|
288
294
|
|
|
289
295
|
"@tokenizer/inflate/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
290
296
|
|
|
291
|
-
"afpnews-api/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
|
292
|
-
|
|
293
297
|
"body-parser/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
|
294
298
|
|
|
295
299
|
"express/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "afpnews-mcp-server",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -12,25 +12,28 @@
|
|
|
12
12
|
"dev": "bun --watch src/index.ts",
|
|
13
13
|
"start": "bun src/index.ts",
|
|
14
14
|
"typecheck": "tsc --noEmit",
|
|
15
|
-
"test": "
|
|
15
|
+
"typecheck:test": "tsc --noEmit -p tsconfig.test.json",
|
|
16
|
+
"test": "bun test src/__tests__/",
|
|
17
|
+
"test:coverage": "bun test --coverage --coverage-reporter=text src/__tests__/",
|
|
18
|
+
"test:coverage:lcov": "bun test --coverage --coverage-reporter=text --coverage-reporter=lcov --coverage-dir coverage src/__tests__/"
|
|
16
19
|
},
|
|
17
20
|
"keywords": [],
|
|
18
21
|
"author": "",
|
|
19
22
|
"license": "ISC",
|
|
20
|
-
"packageManager": "bun@1.3.
|
|
23
|
+
"packageManager": "bun@1.3.11",
|
|
21
24
|
"dependencies": {
|
|
22
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
25
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
23
26
|
"elysia": "^1.4.28",
|
|
24
27
|
"elysia-rate-limit": "^4.5.1",
|
|
25
|
-
"jose": "^6.2.
|
|
28
|
+
"jose": "^6.2.2"
|
|
26
29
|
},
|
|
27
30
|
"peerDependencies": {
|
|
28
31
|
"afpnews-api": "^2.4.0",
|
|
29
32
|
"zod": "^4.3.6"
|
|
30
33
|
},
|
|
31
34
|
"devDependencies": {
|
|
32
|
-
"@types/node": "^25.5.
|
|
33
|
-
"afpnews-api": "^2.4.
|
|
35
|
+
"@types/node": "^25.5.2",
|
|
36
|
+
"afpnews-api": "^2.4.1",
|
|
34
37
|
"zod": "^4.3.6"
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -1,74 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
// Define shared mock functions at module scope
|
|
4
|
-
const authenticateMock = mock();
|
|
5
|
-
const apiCoreInstances: Array<{ token?: unknown; config?: unknown }> = [];
|
|
6
|
-
const registerToolsMock = mock();
|
|
7
|
-
const registerResourcesMock = mock();
|
|
8
|
-
const registerPromptsMock = mock();
|
|
9
|
-
|
|
10
|
-
// mock.module() is hoisted by bun:test before imports are resolved
|
|
11
|
-
mock.module('afpnews-api', () => {
|
|
12
|
-
class MockApiCore {
|
|
13
|
-
token?: unknown;
|
|
14
|
-
config?: unknown;
|
|
15
|
-
|
|
16
|
-
constructor(config?: unknown) {
|
|
17
|
-
this.config = config;
|
|
18
|
-
apiCoreInstances.push(this);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
authenticate = authenticateMock;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return { ApiCore: MockApiCore };
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
mock.module('../tools/index.js', () => ({ registerTools: registerToolsMock }));
|
|
28
|
-
mock.module('../resources/index.js', () => ({ registerResources: registerResourcesMock }));
|
|
29
|
-
mock.module('../prompts/index.js', () => ({ registerPrompts: registerPromptsMock }));
|
|
30
|
-
|
|
31
|
-
// Restore module mocks after all tests in this file
|
|
32
|
-
afterAll(() => mock.restore());
|
|
1
|
+
import { describe, expect, it, mock } from 'bun:test';
|
|
33
2
|
|
|
34
3
|
describe('createServer', () => {
|
|
35
|
-
beforeEach(() => {
|
|
36
|
-
authenticateMock.mockReset();
|
|
37
|
-
authenticateMock.mockResolvedValue(undefined);
|
|
38
|
-
registerToolsMock.mockReset();
|
|
39
|
-
registerResourcesMock.mockReset();
|
|
40
|
-
registerPromptsMock.mockReset();
|
|
41
|
-
apiCoreInstances.length = 0;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
4
|
it('authenticates with provided credentials', async () => {
|
|
45
|
-
|
|
5
|
+
const authenticateMock = mock().mockResolvedValue(undefined);
|
|
6
|
+
|
|
7
|
+
mock.module('afpnews-api', () => ({
|
|
8
|
+
ApiCore: class {
|
|
9
|
+
token?: unknown;
|
|
10
|
+
config?: unknown;
|
|
11
|
+
constructor(config?: unknown) { this.config = config; }
|
|
12
|
+
authenticate = authenticateMock;
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
|
|
46
16
|
const { createServer } = await import('../mcp-server.js');
|
|
47
17
|
await createServer({ apiKey: 'api-key', username: 'user', password: 'pass' });
|
|
48
18
|
|
|
49
|
-
expect(apiCoreInstances).toHaveLength(1);
|
|
50
|
-
expect(apiCoreInstances[0].config).toEqual({ apiKey: 'api-key' });
|
|
51
19
|
expect(authenticateMock).toHaveBeenCalledWith({ username: 'user', password: 'pass' });
|
|
20
|
+
mock.restore();
|
|
52
21
|
});
|
|
53
22
|
|
|
54
23
|
it('passes baseUrl to ApiCore when provided', async () => {
|
|
24
|
+
let capturedConfig: unknown;
|
|
25
|
+
mock.module('afpnews-api', () => ({
|
|
26
|
+
ApiCore: class {
|
|
27
|
+
token?: unknown;
|
|
28
|
+
constructor(config?: unknown) { capturedConfig = config; }
|
|
29
|
+
authenticate = mock().mockResolvedValue(undefined);
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
55
33
|
const { createServer } = await import('../mcp-server.js');
|
|
56
34
|
await createServer({ apiKey: 'api-key', username: 'user', password: 'pass', baseUrl: 'https://custom.api.com' });
|
|
57
35
|
|
|
58
|
-
expect(
|
|
36
|
+
expect(capturedConfig).toEqual({ apiKey: 'api-key', baseUrl: 'https://custom.api.com' });
|
|
37
|
+
mock.restore();
|
|
59
38
|
});
|
|
60
39
|
|
|
61
40
|
it('does not set baseUrl on ApiCore when omitted', async () => {
|
|
41
|
+
let capturedConfig: unknown;
|
|
42
|
+
mock.module('afpnews-api', () => ({
|
|
43
|
+
ApiCore: class {
|
|
44
|
+
token?: unknown;
|
|
45
|
+
constructor(config?: unknown) { capturedConfig = config; }
|
|
46
|
+
authenticate = mock().mockResolvedValue(undefined);
|
|
47
|
+
},
|
|
48
|
+
}));
|
|
49
|
+
|
|
62
50
|
const { createServer } = await import('../mcp-server.js');
|
|
63
51
|
await createServer({ apiKey: 'api-key', username: 'user', password: 'pass' });
|
|
64
52
|
|
|
65
|
-
expect(
|
|
53
|
+
expect(capturedConfig).not.toHaveProperty('baseUrl');
|
|
54
|
+
mock.restore();
|
|
66
55
|
});
|
|
67
56
|
|
|
68
57
|
it('throws on missing credentials', async () => {
|
|
58
|
+
mock.module('afpnews-api', () => ({
|
|
59
|
+
ApiCore: class {
|
|
60
|
+
token?: unknown;
|
|
61
|
+
constructor() {}
|
|
62
|
+
authenticate = mock().mockResolvedValue(undefined);
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
|
|
69
66
|
const { createServer } = await import('../mcp-server.js');
|
|
70
67
|
await expect(
|
|
71
68
|
createServer({ apiKey: 'api-key', username: 'user', password: '' }),
|
|
72
69
|
).rejects.toThrow('Missing authentication');
|
|
70
|
+
mock.restore();
|
|
73
71
|
});
|
|
74
72
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect } from '
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
2
|
import { extractRenditions, formatMediaDocument, formatMediaDocumentsAsJson, formatMediaDocumentsAsCsv, MEDIA_RENDITION_ROLE_MAP } from '../utils/format-media.js';
|
|
3
3
|
|
|
4
4
|
// Fixture bagItem reprenant la structure réelle AFP
|
|
@@ -148,7 +148,7 @@ describe('formatMediaDocumentsAsCsv', () => {
|
|
|
148
148
|
class: 'picture',
|
|
149
149
|
renditions: { thumbnail: { href: 'https://example.com/t.jpg', width: 320, height: 213 } },
|
|
150
150
|
}];
|
|
151
|
-
const r = formatMediaDocumentsAsCsv(docs
|
|
151
|
+
const r = formatMediaDocumentsAsCsv(docs);
|
|
152
152
|
expect(r.content.text).toContain('newsml.test');
|
|
153
153
|
expect(r.content.text).toContain('My Photo');
|
|
154
154
|
expect(r.content.text).toContain('JANE / AFP');
|
|
@@ -161,13 +161,13 @@ describe('formatMediaDocumentsAsCsv', () => {
|
|
|
161
161
|
caption: 'A caption, with "quotes"',
|
|
162
162
|
renditions: {},
|
|
163
163
|
}];
|
|
164
|
-
const r = formatMediaDocumentsAsCsv(docs
|
|
164
|
+
const r = formatMediaDocumentsAsCsv(docs);
|
|
165
165
|
expect(r.content.text).toContain('"A caption, with ""quotes"""');
|
|
166
166
|
});
|
|
167
167
|
|
|
168
168
|
it('handles missing thumbnail href gracefully', () => {
|
|
169
169
|
const docs = [{ uno: 'newsml.test', renditions: {} }];
|
|
170
|
-
const r = formatMediaDocumentsAsCsv(docs
|
|
170
|
+
const r = formatMediaDocumentsAsCsv(docs);
|
|
171
171
|
expect(r.content.text).toContain('newsml.test');
|
|
172
172
|
});
|
|
173
173
|
});
|
|
@@ -179,13 +179,13 @@ describe('formatMediaDocumentsAsJson', () => {
|
|
|
179
179
|
];
|
|
180
180
|
|
|
181
181
|
it('returns { content: TextContent, truncated: boolean }', () => {
|
|
182
|
-
const r = formatMediaDocumentsAsJson(docs
|
|
182
|
+
const r = formatMediaDocumentsAsJson(docs);
|
|
183
183
|
expect(r.content.type).toBe('text');
|
|
184
184
|
expect(typeof r.truncated).toBe('boolean');
|
|
185
185
|
});
|
|
186
186
|
|
|
187
187
|
it('output JSON contains total metadata and documents array', () => {
|
|
188
|
-
const r = formatMediaDocumentsAsJson(docs
|
|
188
|
+
const r = formatMediaDocumentsAsJson(docs, { total: 100, offset: 0 });
|
|
189
189
|
const parsed = JSON.parse(r.content.text);
|
|
190
190
|
expect(parsed.total).toBe(100);
|
|
191
191
|
expect(parsed.offset).toBe(0);
|
|
@@ -196,7 +196,7 @@ describe('formatMediaDocumentsAsJson', () => {
|
|
|
196
196
|
});
|
|
197
197
|
|
|
198
198
|
it('truncated flag is false when content is small', () => {
|
|
199
|
-
const r = formatMediaDocumentsAsJson(docs
|
|
199
|
+
const r = formatMediaDocumentsAsJson(docs, { total: 100, offset: 0 });
|
|
200
200
|
expect(r.truncated).toBe(false);
|
|
201
201
|
});
|
|
202
202
|
|
|
@@ -208,7 +208,7 @@ describe('formatMediaDocumentsAsJson', () => {
|
|
|
208
208
|
caption: 'A very long caption that also takes up considerable space in the JSON output here',
|
|
209
209
|
renditions: { thumbnail: { href: `https://example.com/thumb-${i}.jpg`, width: 320, height: 213 } },
|
|
210
210
|
}));
|
|
211
|
-
const r = formatMediaDocumentsAsJson(largeDocs
|
|
211
|
+
const r = formatMediaDocumentsAsJson(largeDocs, { total: 500, offset: 0 });
|
|
212
212
|
expect(r.truncated).toBe(true);
|
|
213
213
|
const parsed = JSON.parse(r.content.text);
|
|
214
214
|
expect(parsed.shown).toBeLessThan(500);
|
|
@@ -38,10 +38,10 @@ describe('formatDocument', () => {
|
|
|
38
38
|
expect(result.text).not.toContain('Advisory:');
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it('truncates to
|
|
41
|
+
it('truncates to 2 paragraphs when fullText=false', () => {
|
|
42
42
|
const result = formatDocument(FIXTURE_DOC, false);
|
|
43
|
-
expect(result.text).toContain('
|
|
44
|
-
expect(result.text).not.toContain('
|
|
43
|
+
expect(result.text).toContain('Second paragraph with more details.');
|
|
44
|
+
expect(result.text).not.toContain('Third paragraph continues.');
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
it('includes all paragraphs when fullText=true', () => {
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, mock } from 'bun:test';
|
|
2
|
+
import { afpGetMediaTool } from '../tools/get-media.js';
|
|
3
|
+
|
|
4
|
+
const ORIGINAL_FETCH = globalThis.fetch;
|
|
5
|
+
|
|
6
|
+
function getText(result: any, index = 0): string {
|
|
7
|
+
return result.content[index]?.text ?? '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('afpGetMediaTool.handler', () => {
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
globalThis.fetch = ORIGINAL_FETCH;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('returns error when media document is missing', async () => {
|
|
16
|
+
const apicore = { get: mock().mockResolvedValue(undefined) };
|
|
17
|
+
|
|
18
|
+
const result = await afpGetMediaTool.handler(apicore, { uno: 'MISSING' });
|
|
19
|
+
|
|
20
|
+
expect('isError' in result && result.isError).toBe(true);
|
|
21
|
+
expect(getText(result)).toContain('Media document not found');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns metadata only when embed=false', async () => {
|
|
25
|
+
const apicore = {
|
|
26
|
+
get: mock().mockResolvedValue({
|
|
27
|
+
uno: 'MEDIA-001',
|
|
28
|
+
title: 'Photo',
|
|
29
|
+
class: 'picture',
|
|
30
|
+
creditLine: 'AFP',
|
|
31
|
+
bagItem: [{ medias: [{ role: 'Thumbnail', href: 'https://example.com/thumb.jpg', width: 320, height: 200, type: 'Photo' }] }],
|
|
32
|
+
}),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const result = await afpGetMediaTool.handler(apicore, { uno: 'MEDIA-001' });
|
|
36
|
+
|
|
37
|
+
expect(result.content).toHaveLength(1);
|
|
38
|
+
expect(getText(result)).toContain('## Photo');
|
|
39
|
+
expect(getText(result)).toContain('thumbnail: https://example.com/thumb.jpg');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('warns when trying to embed an SVG graphic', async () => {
|
|
43
|
+
const apicore = {
|
|
44
|
+
get: mock().mockResolvedValue({
|
|
45
|
+
uno: 'GRAPHIC-001',
|
|
46
|
+
title: 'Graphic',
|
|
47
|
+
class: 'graphic',
|
|
48
|
+
bagItem: [{ medias: [{ role: 'Preview', href: 'https://example.com/graphic.svg', width: 1200, height: 800, type: 'Graphic' }] }],
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const result = await afpGetMediaTool.handler(apicore, { uno: 'GRAPHIC-001', embed: true });
|
|
53
|
+
|
|
54
|
+
expect(result.content).toHaveLength(2);
|
|
55
|
+
expect(getText(result, 1)).toContain('SVG graphics cannot be embedded');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('warns when no rendition is available for embedding', async () => {
|
|
59
|
+
const apicore = {
|
|
60
|
+
get: mock().mockResolvedValue({
|
|
61
|
+
uno: 'MEDIA-EMPTY',
|
|
62
|
+
title: 'Empty',
|
|
63
|
+
class: 'picture',
|
|
64
|
+
bagItem: [],
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const result = await afpGetMediaTool.handler(apicore, { uno: 'MEDIA-EMPTY', embed: true });
|
|
69
|
+
|
|
70
|
+
expect(result.content).toHaveLength(2);
|
|
71
|
+
expect(getText(result, 1)).toContain('no rendition available');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('embeds a video thumbnail and adds the poster-frame note', async () => {
|
|
75
|
+
globalThis.fetch = mock().mockResolvedValue({
|
|
76
|
+
ok: true,
|
|
77
|
+
arrayBuffer: async () => new Uint8Array([1, 2, 3]).buffer,
|
|
78
|
+
headers: new Headers({ 'content-type': 'image/jpeg' }),
|
|
79
|
+
} as Response);
|
|
80
|
+
|
|
81
|
+
const apicore = {
|
|
82
|
+
get: mock().mockResolvedValue({
|
|
83
|
+
uno: 'VIDEO-001',
|
|
84
|
+
title: 'Video',
|
|
85
|
+
class: 'video',
|
|
86
|
+
bagItem: [{ medias: [{ role: 'Thumbnail', href: 'https://example.com/poster.jpg', width: 320, height: 180, type: 'Photo' }] }],
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const result = await afpGetMediaTool.handler(apicore, { uno: 'VIDEO-001', embed: true, rendition: 'highdef' });
|
|
91
|
+
|
|
92
|
+
expect(result.content).toHaveLength(2);
|
|
93
|
+
expect(getText(result)).toContain('poster frame only');
|
|
94
|
+
expect(result.content[1]).toEqual(expect.objectContaining({ type: 'image', mimeType: 'image/jpeg' }));
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('returns a warning when image fetch fails', async () => {
|
|
98
|
+
globalThis.fetch = mock().mockResolvedValue({
|
|
99
|
+
ok: false,
|
|
100
|
+
status: 403,
|
|
101
|
+
statusText: 'Forbidden',
|
|
102
|
+
headers: new Headers(),
|
|
103
|
+
} as Response);
|
|
104
|
+
|
|
105
|
+
const apicore = {
|
|
106
|
+
get: mock().mockResolvedValue({
|
|
107
|
+
uno: 'MEDIA-403',
|
|
108
|
+
title: 'Photo',
|
|
109
|
+
class: 'picture',
|
|
110
|
+
bagItem: [{ medias: [{ role: 'Preview', href: 'https://example.com/prev.jpg', width: 1200, height: 800, type: 'Photo' }] }],
|
|
111
|
+
}),
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const result = await afpGetMediaTool.handler(apicore, { uno: 'MEDIA-403', embed: true });
|
|
115
|
+
|
|
116
|
+
expect(result.content).toHaveLength(2);
|
|
117
|
+
expect(getText(result, 1)).toContain('image embed failed');
|
|
118
|
+
expect(getText(result, 1)).toContain('HTTP 403');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect, mock } from 'bun:test';
|
|
2
|
+
import { afpListFacetsTool } from '../tools/list-facets.js';
|
|
3
|
+
|
|
4
|
+
describe('afpListFacetsTool', () => {
|
|
5
|
+
it('returns no-results message when list is empty', async () => {
|
|
6
|
+
const apicore = { list: mock().mockResolvedValue({ keywords: [] }) };
|
|
7
|
+
|
|
8
|
+
const result = await afpListFacetsTool.handler(apicore, { facet: 'slug' });
|
|
9
|
+
|
|
10
|
+
expect(result.content).toHaveLength(1);
|
|
11
|
+
expect(result.content[0]!.text).toContain('No facet values found');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('applies AFP text defaults with explicit lang outside preset', async () => {
|
|
15
|
+
const apicore = { list: mock().mockResolvedValue({ keywords: [{ name: 'economy', count: 42 }] }) };
|
|
16
|
+
|
|
17
|
+
await afpListFacetsTool.handler(apicore, { facet: 'slug', lang: 'en' });
|
|
18
|
+
|
|
19
|
+
const [, params] = apicore.list.mock.calls[0]!;
|
|
20
|
+
expect(params).toEqual({ class: ['text'], provider: ['afp'], langs: ['en'], size: 10 });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('adds truncation hint in json output when facet list is too large', async () => {
|
|
24
|
+
const keywords = Array.from({ length: 3000 }, (_, i) => ({ name: `topic-${i}-${'x'.repeat(30)}`, count: i + 1 }));
|
|
25
|
+
const apicore = { list: mock().mockResolvedValue({ keywords }) };
|
|
26
|
+
|
|
27
|
+
const result = await afpListFacetsTool.handler(apicore, { facet: 'slug', format: 'json' });
|
|
28
|
+
|
|
29
|
+
expect(result.content.length).toBeGreaterThan(1);
|
|
30
|
+
expect(result.content[1]!.text).toContain('Response truncated');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('adds truncation hint in csv output when facet list is too large', async () => {
|
|
34
|
+
const keywords = Array.from({ length: 3000 }, (_, i) => ({ name: `topic-${i}-${'x'.repeat(30)}`, count: i + 1 }));
|
|
35
|
+
const apicore = { list: mock().mockResolvedValue({ keywords }) };
|
|
36
|
+
|
|
37
|
+
const result = await afpListFacetsTool.handler(apicore, { facet: 'slug', format: 'csv' });
|
|
38
|
+
|
|
39
|
+
expect(result.content.length).toBeGreaterThan(1);
|
|
40
|
+
expect(result.content[1]!.text).toContain('Response truncated');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('adds truncation hint in markdown output when facet list is too large', async () => {
|
|
44
|
+
const keywords = Array.from({ length: 3000 }, (_, i) => ({ name: `topic-${i}-${'x'.repeat(30)}`, count: i + 1 }));
|
|
45
|
+
const apicore = { list: mock().mockResolvedValue({ keywords }) };
|
|
46
|
+
|
|
47
|
+
const result = await afpListFacetsTool.handler(apicore, { facet: 'slug' });
|
|
48
|
+
|
|
49
|
+
expect(result.content.length).toBeGreaterThan(1);
|
|
50
|
+
expect(result.content[1]!.text).toContain('Response truncated');
|
|
51
|
+
});
|
|
52
|
+
});
|