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.
@@ -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 cpp csharp
7
- # csharp_omnisharp dart elixir elm erlang
8
- # fortran fsharp go groovy haskell
9
- # java julia kotlin lua markdown
10
- # matlab nix pascal perl php
11
- # php_phpactor powershell python python_jedi r
12
- # rego ruby ruby_solargraph rust scala
13
- # swift terraform toml typescript typescript_vts
14
- # vue yaml zig
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. We recommend not excluding any tools, see the readme for more details.
56
- # Below is the complete list of tools for convenience.
57
- # To make sure you have the latest list of tools, and to view their descriptions,
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 4 paragraphs). Set `fullText: true` to get the complete article body. Presets default to full text.
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.27.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.1",
13
- "zod": "^4.3.6",
11
+ "jose": "^6.2.2",
14
12
  },
15
13
  "devDependencies": {
16
- "@types/node": "^25.5.0",
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.27.1", "", { "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-sr6GbP+4edBwFndLbM60gf07z0FQ79gaExpnsjMGePXqFcSSb7t6iscpjk9DhFhwd+mTEQrzNafGP8/iGGFYaA=="],
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.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
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.0", "", { "dependencies": { "@types/btoa-lite": "1.0.2", "@types/moo": "0.5.10", "@types/nearley": "2.11.5", "@types/node": "25.2.3", "@types/statuses": "2.0.6", "btoa-lite": "1.0.0", "events": "3.3.0", "moo": "0.5.2", "nearley": "2.20.1", "statuses": "2.0.2", "zod": "4.3.6" } }, "sha512-VErqy0P5h2sjVjaSoIHF1uFsKx16PYKZUFBCCUmHsPdPM99lQhk0ClibPU7Wk9i34BtUiqLcQxrmTdCOj9ikpA=="],
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.1", "", {}, "sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw=="],
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.2", "", {}, "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q=="],
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.2.3", "", { "dependencies": { "undici-types": "7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="],
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.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": "bun test src/__tests__/format.test.ts src/__tests__/format-media.test.ts src/__tests__/types-media.test.ts src/__tests__/index-auth.test.ts src/__tests__/server.test.ts src/__tests__/get-media.test.ts && bun test src/__tests__/create-server.test.ts"
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.10",
23
+ "packageManager": "bun@1.3.11",
21
24
  "dependencies": {
22
- "@modelcontextprotocol/sdk": "^1.27.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.1"
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.0",
33
- "afpnews-api": "^2.4.0",
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 { afterAll, beforeEach, describe, expect, it, mock } from 'bun:test';
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
- // Dynamic import ensures mock.module() is active when the module is loaded
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(apiCoreInstances[0].config).toEqual({ apiKey: 'api-key', baseUrl: 'https://custom.api.com' });
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(apiCoreInstances[0].config).not.toHaveProperty('baseUrl');
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 'vitest';
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 as any);
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 as any);
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 as any);
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 as any);
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 as any, { total: 100, offset: 0 });
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 as any, { total: 100, offset: 0 });
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 as any, { total: 500, offset: 0 });
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 4 paragraphs when fullText=false', () => {
41
+ it('truncates to 2 paragraphs when fullText=false', () => {
42
42
  const result = formatDocument(FIXTURE_DOC, false);
43
- expect(result.text).toContain('Fourth paragraph wraps up.');
44
- expect(result.text).not.toContain('Fifth paragraph is extra content.');
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
+ });
@@ -1,4 +1,4 @@
1
- import { describe, it, expect } from 'vitest';
1
+ import { describe, it, expect } from 'bun:test';
2
2
  import { inferMimeType, selectRenditionForEmbed } from '../tools/get-media.js';
3
3
  import type { MediaRenditions } from '../utils/types.js';
4
4