afpnews-mcp-server 2.0.0 → 2.0.2
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 +57 -52
- 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/prompts/country-news.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 +22 -12
- package/src/tools/search-media.ts +24 -56
- 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/docs/plans/2026-03-08-auth-refactor.md +0 -740
- package/docs/superpowers/plans/2026-03-17-bun-elysia-migration.md +0 -1067
- package/docs/superpowers/plans/2026-03-17-media-tools.md +0 -1259
- package/docs/superpowers/specs/2026-03-17-bun-elysia-migration-design.md +0 -220
- package/docs/superpowers/specs/2026-03-17-media-tools-design.md +0 -404
package/.serena/project.yml
CHANGED
|
@@ -3,21 +3,26 @@ project_name: "afpnews-mcp-server"
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
# list of languages for which language servers are started; choose from:
|
|
6
|
-
# al bash clojure
|
|
7
|
-
# csharp_omnisharp
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
6
|
+
# al angular ansible bash clojure
|
|
7
|
+
# cpp cpp_ccls crystal csharp csharp_omnisharp
|
|
8
|
+
# dart elixir elm erlang fortran
|
|
9
|
+
# fsharp go groovy haskell haxe
|
|
10
|
+
# hlsl html java json julia
|
|
11
|
+
# kotlin lean4 lua luau markdown
|
|
12
|
+
# matlab msl nix ocaml pascal
|
|
13
|
+
# perl php php_phpactor powershell python
|
|
14
|
+
# python_jedi python_ty r rego ruby
|
|
15
|
+
# ruby_solargraph rust scala scss solidity
|
|
16
|
+
# swift systemverilog terraform toml typescript
|
|
17
|
+
# typescript_vts vue yaml zig
|
|
15
18
|
# (This list may be outdated. For the current list, see values of Language enum here:
|
|
16
19
|
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
|
17
20
|
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
|
18
21
|
# Note:
|
|
19
22
|
# - For C, use cpp
|
|
20
23
|
# - For JavaScript, use typescript
|
|
24
|
+
# - For Angular projects, use angular (subsumes typescript+html; requires `npm install` in the project root)
|
|
25
|
+
# - For SCSS / Sass / plain CSS, use scss (some-sass-language-server handles all three)
|
|
21
26
|
# - For Free Pascal/Lazarus, use pascal
|
|
22
27
|
# Special requirements:
|
|
23
28
|
# Some languages require additional setup/installations.
|
|
@@ -52,52 +57,19 @@ ignored_paths: []
|
|
|
52
57
|
# Added on 2025-04-18
|
|
53
58
|
read_only: false
|
|
54
59
|
|
|
55
|
-
# list of tool names to exclude.
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
# execute `uv run scripts/print_tool_overview.py`.
|
|
59
|
-
#
|
|
60
|
-
# * `activate_project`: Activates a project by name.
|
|
61
|
-
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
|
62
|
-
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
|
63
|
-
# * `delete_lines`: Deletes a range of lines within a file.
|
|
64
|
-
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
|
65
|
-
# * `execute_shell_command`: Executes a shell command.
|
|
66
|
-
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
|
67
|
-
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
|
68
|
-
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
|
69
|
-
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
|
70
|
-
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
|
71
|
-
# * `initial_instructions`: Gets the initial instructions for the current project.
|
|
72
|
-
# Should only be used in settings where the system prompt cannot be set,
|
|
73
|
-
# e.g. in clients you have no control over, like Claude Desktop.
|
|
74
|
-
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
|
75
|
-
# * `insert_at_line`: Inserts content at a given line in a file.
|
|
76
|
-
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
|
77
|
-
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
|
78
|
-
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
|
79
|
-
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
|
80
|
-
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
|
81
|
-
# * `read_file`: Reads a file within the project directory.
|
|
82
|
-
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
|
83
|
-
# * `remove_project`: Removes a project from the Serena configuration.
|
|
84
|
-
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
|
85
|
-
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
|
86
|
-
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
|
87
|
-
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
|
88
|
-
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
|
89
|
-
# * `switch_modes`: Activates modes by providing a list of their names
|
|
90
|
-
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
|
91
|
-
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
|
92
|
-
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
|
93
|
-
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
|
60
|
+
# list of tool names to exclude.
|
|
61
|
+
# This extends the existing exclusions (e.g. from the global configuration)
|
|
62
|
+
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
|
94
63
|
excluded_tools: []
|
|
95
64
|
|
|
96
|
-
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default)
|
|
65
|
+
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
|
66
|
+
# This extends the existing inclusions (e.g. from the global configuration).
|
|
67
|
+
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
|
97
68
|
included_optional_tools: []
|
|
98
69
|
|
|
99
70
|
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
|
100
71
|
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
|
72
|
+
# Find the list of tools here: https://oraios.github.io/serena/01-about/035_tools.html
|
|
101
73
|
fixed_tools: []
|
|
102
74
|
|
|
103
75
|
# list of mode names to that are always to be included in the set of active modes
|
|
@@ -108,11 +80,14 @@ fixed_tools: []
|
|
|
108
80
|
# Set this to a list of mode names to always include the respective modes for this project.
|
|
109
81
|
base_modes:
|
|
110
82
|
|
|
111
|
-
# list of mode names that are to be activated by default.
|
|
112
|
-
# The full set of modes to be activated is base_modes + default_modes.
|
|
113
|
-
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
|
83
|
+
# list of mode names that are to be activated by default, overriding the setting in the global configuration.
|
|
84
|
+
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
|
85
|
+
# If the setting is undefined/empty, the default_modes from the global configuration (serena_config.yml) apply.
|
|
114
86
|
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
|
87
|
+
# Therefore, you can set this to [] if you do not want the default modes defined in the global config to apply
|
|
88
|
+
# for this project.
|
|
115
89
|
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
|
90
|
+
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
|
116
91
|
default_modes:
|
|
117
92
|
|
|
118
93
|
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
|
@@ -133,3 +108,33 @@ read_only_memory_patterns: []
|
|
|
133
108
|
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
|
134
109
|
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
|
135
110
|
line_ending:
|
|
111
|
+
|
|
112
|
+
# list of regex patterns for memories to completely ignore.
|
|
113
|
+
# Matching memories will not appear in list_memories or activate_project output
|
|
114
|
+
# and cannot be accessed via read_memory or write_memory.
|
|
115
|
+
# To access ignored memory files, use the read_file tool on the raw file path.
|
|
116
|
+
# Extends the list from the global configuration, merging the two lists.
|
|
117
|
+
# Example: ["_archive/.*", "_episodes/.*"]
|
|
118
|
+
ignored_memory_patterns: []
|
|
119
|
+
|
|
120
|
+
# advanced configuration option allowing to configure language server-specific options.
|
|
121
|
+
# Maps the language key to the options.
|
|
122
|
+
# 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.
|
|
123
|
+
# No documentation on options means no options are available.
|
|
124
|
+
ls_specific_settings: {}
|
|
125
|
+
|
|
126
|
+
# list of mode names to be activated additionally for this project, e.g. ["query-projects"]
|
|
127
|
+
# The full set of modes to be activated is base_modes (from global config) + default_modes + added_modes.
|
|
128
|
+
# See https://oraios.github.io/serena/02-usage/050_configuration.html#modes
|
|
129
|
+
added_modes:
|
|
130
|
+
|
|
131
|
+
# list of additional workspace folder paths for cross-package reference support (e.g. in monorepos).
|
|
132
|
+
# Paths can be absolute or relative to the project root.
|
|
133
|
+
# Each folder is registered as an LSP workspace folder, enabling language servers to discover
|
|
134
|
+
# symbols and references across package boundaries.
|
|
135
|
+
# Currently supported for: TypeScript.
|
|
136
|
+
# Example:
|
|
137
|
+
# additional_workspace_folders:
|
|
138
|
+
# - ../sibling-package
|
|
139
|
+
# - ../shared-lib
|
|
140
|
+
additional_workspace_folders: []
|
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.2",
|
|
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
|
+
});
|