afpnews-mcp-server 2.0.1 → 2.1.0
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 +43 -52
- package/package.json +3 -3
- package/src/__tests__/create-server.test.ts +5 -0
- package/src/__tests__/fixtures.ts +14 -0
- package/src/__tests__/format.test.ts +35 -2
- package/src/__tests__/server.test.ts +19 -1
- package/src/prompts/country-news.ts +1 -1
- package/src/tools/get-article.ts +11 -3
- package/src/tools/search-articles.ts +2 -2
- package/src/tools/search-media.ts +3 -3
- package/src/utils/format.ts +26 -1
- 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
|
|
@@ -147,3 +122,19 @@ ignored_memory_patterns: []
|
|
|
147
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.
|
|
148
123
|
# No documentation on options means no options are available.
|
|
149
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "afpnews-mcp-server",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"jose": "^6.2.2"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"afpnews-api": "^2.
|
|
31
|
+
"afpnews-api": "^2.5.0",
|
|
32
32
|
"zod": "^4.3.6"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^25.5.2",
|
|
36
|
-
"afpnews-api": "^2.
|
|
36
|
+
"afpnews-api": "^2.5.0",
|
|
37
37
|
"zod": "^4.3.6"
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { describe, expect, it, mock } from 'bun:test';
|
|
2
|
+
import * as actualApi from 'afpnews-api';
|
|
2
3
|
|
|
3
4
|
describe('createServer', () => {
|
|
4
5
|
it('authenticates with provided credentials', async () => {
|
|
5
6
|
const authenticateMock = mock().mockResolvedValue(undefined);
|
|
6
7
|
|
|
7
8
|
mock.module('afpnews-api', () => ({
|
|
9
|
+
...actualApi,
|
|
8
10
|
ApiCore: class {
|
|
9
11
|
token?: unknown;
|
|
10
12
|
config?: unknown;
|
|
@@ -23,6 +25,7 @@ describe('createServer', () => {
|
|
|
23
25
|
it('passes baseUrl to ApiCore when provided', async () => {
|
|
24
26
|
let capturedConfig: unknown;
|
|
25
27
|
mock.module('afpnews-api', () => ({
|
|
28
|
+
...actualApi,
|
|
26
29
|
ApiCore: class {
|
|
27
30
|
token?: unknown;
|
|
28
31
|
constructor(config?: unknown) { capturedConfig = config; }
|
|
@@ -40,6 +43,7 @@ describe('createServer', () => {
|
|
|
40
43
|
it('does not set baseUrl on ApiCore when omitted', async () => {
|
|
41
44
|
let capturedConfig: unknown;
|
|
42
45
|
mock.module('afpnews-api', () => ({
|
|
46
|
+
...actualApi,
|
|
43
47
|
ApiCore: class {
|
|
44
48
|
token?: unknown;
|
|
45
49
|
constructor(config?: unknown) { capturedConfig = config; }
|
|
@@ -56,6 +60,7 @@ describe('createServer', () => {
|
|
|
56
60
|
|
|
57
61
|
it('throws on missing credentials', async () => {
|
|
58
62
|
mock.module('afpnews-api', () => ({
|
|
63
|
+
...actualApi,
|
|
59
64
|
ApiCore: class {
|
|
60
65
|
token?: unknown;
|
|
61
66
|
constructor() {}
|
|
@@ -27,6 +27,20 @@ export const FIXTURE_DOC_MINIMAL: AFPDocument = {
|
|
|
27
27
|
news: ['Only one paragraph.'],
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
export const FIXTURE_VIDEO_DOC: AFPDocument = {
|
|
31
|
+
uno: 'AFP-TEST-VID-001',
|
|
32
|
+
headline: 'Test Video Headline',
|
|
33
|
+
published: '2026-02-14T10:30:00Z',
|
|
34
|
+
lang: 'fr',
|
|
35
|
+
genre: 'STOCKSHOTS',
|
|
36
|
+
class: 'video',
|
|
37
|
+
news: [
|
|
38
|
+
'1. 00:00-00:12 Vue aérienne de la ville',
|
|
39
|
+
'2. 00:12-00:30 SOUNDBITE 1 - Jean Dupont, témoin',
|
|
40
|
+
'"Tout a commencé très vite"',
|
|
41
|
+
],
|
|
42
|
+
};
|
|
43
|
+
|
|
30
44
|
export function makeDocs(count: number): AFPDocument[] {
|
|
31
45
|
return Array.from({ length: count }, (_, i) => ({
|
|
32
46
|
uno: `AFP-TEST-${String(i + 1).padStart(3, '0')}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from 'bun:test';
|
|
2
|
-
import { formatDocument, MARKDOWN_API_FIELDS } from '../utils/format.js';
|
|
3
|
-
import { FIXTURE_DOC, FIXTURE_DOC_MINIMAL } from './fixtures.js';
|
|
2
|
+
import { formatDocument, formatFullArticle, formatShotList, MARKDOWN_API_FIELDS } from '../utils/format.js';
|
|
3
|
+
import { FIXTURE_DOC, FIXTURE_DOC_MINIMAL, FIXTURE_VIDEO_DOC } from './fixtures.js';
|
|
4
4
|
|
|
5
5
|
describe('formatDocument', () => {
|
|
6
6
|
it('returns { type: "text", text: string }', () => {
|
|
@@ -64,3 +64,36 @@ describe('MARKDOWN_API_FIELDS', () => {
|
|
|
64
64
|
expect(MARKDOWN_API_FIELDS).not.toContain('afpshortid');
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
describe('formatShotList', () => {
|
|
69
|
+
it('renders a timecoded shot list for a video document', () => {
|
|
70
|
+
const out = formatShotList(FIXTURE_VIDEO_DOC);
|
|
71
|
+
expect(out).toContain('## Shot list');
|
|
72
|
+
expect(out).toContain('1. [00:00-00:12] Vue aérienne de la ville');
|
|
73
|
+
expect(out).toContain('2. [00:12-00:30] Jean Dupont, témoin');
|
|
74
|
+
expect(out).toContain(' "Tout a commencé très vite"');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('returns null for non-video documents', () => {
|
|
78
|
+
expect(formatShotList(FIXTURE_DOC)).toBeNull();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('returns null for a video whose news has no parseable shot', () => {
|
|
82
|
+
expect(formatShotList({ class: 'video', news: ['no timecode here'] })).toBeNull();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('formatFullArticle', () => {
|
|
87
|
+
it('uses the shot list as body for a video document', () => {
|
|
88
|
+
const result = formatFullArticle(FIXTURE_VIDEO_DOC);
|
|
89
|
+
expect(result.text).toContain('## Shot list');
|
|
90
|
+
expect(result.text).toContain('[00:00-00:12] Vue aérienne de la ville');
|
|
91
|
+
expect(result.text).toContain('**Class:** video');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('keeps the plain text body for non-video documents', () => {
|
|
95
|
+
const result = formatFullArticle(FIXTURE_DOC);
|
|
96
|
+
expect(result.text).not.toContain('## Shot list');
|
|
97
|
+
expect(result.text).toContain('First paragraph of the article.');
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -6,7 +6,7 @@ import { registerTools } from '../tools/index.js';
|
|
|
6
6
|
import { registerResources } from '../resources/index.js';
|
|
7
7
|
import { registerPrompts } from '../prompts/index.js';
|
|
8
8
|
import type { ServerContext } from '../mcp-server.js';
|
|
9
|
-
import { FIXTURE_DOC, makeDocs } from './fixtures.js';
|
|
9
|
+
import { FIXTURE_DOC, FIXTURE_VIDEO_DOC, makeDocs } from './fixtures.js';
|
|
10
10
|
|
|
11
11
|
function createMockApicore() {
|
|
12
12
|
return {
|
|
@@ -256,6 +256,24 @@ describe('MCP integration', () => {
|
|
|
256
256
|
expect(text).not.toContain('**Country:**');
|
|
257
257
|
});
|
|
258
258
|
|
|
259
|
+
it('renders a timecoded shot list body for video documents', async () => {
|
|
260
|
+
apicore.get.mockResolvedValueOnce(FIXTURE_VIDEO_DOC);
|
|
261
|
+
const result = await client.callTool({ name: 'afp_get_article', arguments: { uno: 'AFP-TEST-VID-001' } });
|
|
262
|
+
const text = getText(result);
|
|
263
|
+
expect(text).toContain('## Shot list');
|
|
264
|
+
expect(text).toContain('1. [00:00-00:12] Vue aérienne de la ville');
|
|
265
|
+
expect(text).toContain('"Tout a commencé très vite"');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('returns the raw document as json when format=json', async () => {
|
|
269
|
+
apicore.get.mockResolvedValueOnce(FIXTURE_DOC);
|
|
270
|
+
const result = await client.callTool({ name: 'afp_get_article', arguments: { uno: 'AFP-TEST-001', format: 'json' } });
|
|
271
|
+
const text = getText(result);
|
|
272
|
+
const parsed = JSON.parse(text);
|
|
273
|
+
expect(parsed.uno).toBe('AFP-TEST-001');
|
|
274
|
+
expect(parsed.news).toEqual(FIXTURE_DOC.news);
|
|
275
|
+
});
|
|
276
|
+
|
|
259
277
|
it('returns isError on API failure', async () => {
|
|
260
278
|
apicore.get.mockRejectedValueOnce(new Error('Not found'));
|
|
261
279
|
const result = await client.callTool({ name: 'afp_get_article', arguments: { uno: 'BAD-UNO' } });
|
|
@@ -13,7 +13,7 @@ export const countryNewsPrompt = {
|
|
|
13
13
|
role: 'user' as const,
|
|
14
14
|
content: {
|
|
15
15
|
type: 'text' as const,
|
|
16
|
-
text: `Use afp_search_articles to find recent news for country "${country}" (facets: {
|
|
16
|
+
text: `Use afp_search_articles to find recent news for country "${country}" (facets: { langs: ["${lang}"], country: ["${country}"] }, size: 15). Write a news summary for this country covering the main stories of the day.`,
|
|
17
17
|
},
|
|
18
18
|
}],
|
|
19
19
|
}),
|
package/src/tools/get-article.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { ApiCore } from 'afpnews-api';
|
|
3
|
-
import { formatFullArticle, toolError } from '../utils/format.js';
|
|
3
|
+
import { formatFullArticle, textContent, toolError } from '../utils/format.js';
|
|
4
4
|
import { formatErrorMessage, UNO_FORMAT_NOTE } from './shared.js';
|
|
5
5
|
|
|
6
6
|
const inputSchema = z.object({
|
|
7
7
|
uno: z.string().describe('The unique UNO identifier of the article'),
|
|
8
|
+
format: z
|
|
9
|
+
.enum(['markdown', 'json'])
|
|
10
|
+
.optional()
|
|
11
|
+
.describe('Output format: markdown (default, rendered article/shot list) or json (raw document, parse it yourself).'),
|
|
8
12
|
});
|
|
9
13
|
|
|
10
14
|
type GetArticleInput = z.infer<typeof inputSchema>;
|
|
@@ -37,11 +41,15 @@ Returns:
|
|
|
37
41
|
- Full article body (all paragraphs, no truncation)
|
|
38
42
|
|
|
39
43
|
Example:
|
|
40
|
-
{ uno: "newsml.afp.com.20260222T090659Z.doc-98hu39e" }
|
|
44
|
+
{ uno: "newsml.afp.com.20260222T090659Z.doc-98hu39e" }
|
|
45
|
+
{ uno: "newsml.afp.com.20260222T090659Z.doc-98hu39e", format: "json" }`,
|
|
41
46
|
inputSchema,
|
|
42
|
-
handler: async (apicore: Pick<ApiCore, 'get'>, { uno }: GetArticleInput) => {
|
|
47
|
+
handler: async (apicore: Pick<ApiCore, 'get'>, { uno, format = 'markdown' }: GetArticleInput) => {
|
|
43
48
|
try {
|
|
44
49
|
const doc = await apicore.get(uno);
|
|
50
|
+
if (format === 'json') {
|
|
51
|
+
return { content: [textContent(JSON.stringify(doc, null, 2))] };
|
|
52
|
+
}
|
|
45
53
|
return { content: [formatFullArticle(doc)] };
|
|
46
54
|
} catch (error) {
|
|
47
55
|
return toolError(formatErrorMessage(`fetching article "${uno}"`, error, 'Verify the UNO identifier is correct.'));
|
|
@@ -40,7 +40,7 @@ const inputSchema = z.object({
|
|
|
40
40
|
size: z.number().optional().describe('Number of results to return (default 10, max 1000)'),
|
|
41
41
|
sortOrder: z.enum(['asc', 'desc']).optional().describe("Sort order by date (default 'desc')"),
|
|
42
42
|
offset: z.number().optional().describe('Offset for pagination (number of results to skip)'),
|
|
43
|
-
facets: z.record(z.string(), facetParamValueSchema).optional().describe("Facet filters passed to the AFP query (e.g. {
|
|
43
|
+
facets: z.record(z.string(), facetParamValueSchema).optional().describe("Facet filters passed to the AFP query (e.g. { langs: ['fr'], dateFrom: '2026-01-01', dateTo: '2026-01-31', country: ['usa'], genre: 'Papier général', urgency: 1 })."),
|
|
44
44
|
}).strict().superRefine((value, ctx) => {
|
|
45
45
|
for (const key of Object.keys(value.facets ?? {})) {
|
|
46
46
|
if (reservedFacetKeys.has(key)) {
|
|
@@ -88,7 +88,7 @@ Returns:
|
|
|
88
88
|
- csv: Header row + data rows with selected fields
|
|
89
89
|
|
|
90
90
|
Examples:
|
|
91
|
-
- Latest Ukraine news: { query: "Ukraine", facets: {
|
|
91
|
+
- Latest Ukraine news: { query: "Ukraine", facets: { langs: ["en"] }, size: 5 }
|
|
92
92
|
- French front page: { preset: "a-la-une" }
|
|
93
93
|
- Export metadata as CSV: { query: "economy", format: "csv", fields: ["uno", "headline", "country"] }`,
|
|
94
94
|
inputSchema,
|
|
@@ -29,7 +29,7 @@ const inputSchema = z.object({
|
|
|
29
29
|
sortOrder: z.enum(['asc', 'desc']).optional().describe("Sort order by date (default 'desc')"),
|
|
30
30
|
format: outputFormatEnum.optional().describe('Output format: markdown (default), json, or csv'),
|
|
31
31
|
facets: z.record(z.string(), facetParamValueSchema).optional().describe(
|
|
32
|
-
"Additional AFP facet filters (e.g. {
|
|
32
|
+
"Additional AFP facet filters (e.g. { langs: ['fr'], country: ['fra'], dateFrom: '2026-01-01' })"
|
|
33
33
|
),
|
|
34
34
|
}).strict().superRefine((value, ctx) => {
|
|
35
35
|
for (const key of Object.keys(value.facets ?? {})) {
|
|
@@ -65,7 +65,7 @@ Args:
|
|
|
65
65
|
- offset: Pagination offset
|
|
66
66
|
- sortOrder: 'asc' or 'desc' (default 'desc')
|
|
67
67
|
- format: markdown (default, with inline thumbnails), json (structured with rendition URLs), csv
|
|
68
|
-
- facets: Additional AFP filters (e.g. {
|
|
68
|
+
- facets: Additional AFP filters (e.g. { langs: ['fr'], country: ['fra'], dateFrom: '2026-01-01' }).
|
|
69
69
|
Defaults: provider=afp. Override provider only when partner media is explicitly needed.
|
|
70
70
|
|
|
71
71
|
Pagination:
|
|
@@ -84,7 +84,7 @@ Rendition sizes:
|
|
|
84
84
|
|
|
85
85
|
Examples:
|
|
86
86
|
- AFP football photos: { class: "picture", query: "football" }
|
|
87
|
-
- French infographics on economy: { class: "graphic", query: "économie", facets: {
|
|
87
|
+
- French infographics on economy: { class: "graphic", query: "économie", facets: { langs: ["fr"] } }
|
|
88
88
|
- All media on a topic: { query: "climate protest", format: "json" }
|
|
89
89
|
- Export gallery CSV: { class: "picture", query: "Paris", format: "csv" }`,
|
|
90
90
|
inputSchema,
|
package/src/utils/format.ts
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
|
+
import { parseShotList } from 'afpnews-api';
|
|
1
2
|
import type { AFPDocument, TextContent } from './types.js';
|
|
2
3
|
import { EXCERPT_PARAGRAPH_COUNT, CHARACTER_LIMIT } from './types.js';
|
|
3
4
|
|
|
5
|
+
const VIDEO_CLASSES = new Set(['video', 'videography']);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Render a video shot list (timecodes + descriptions + soundbite quotes) from
|
|
9
|
+
* the raw `news` lines. Returns `null` when the document is not a video or when
|
|
10
|
+
* no shot could be parsed, so callers can fall back to the plain text body.
|
|
11
|
+
*/
|
|
12
|
+
export function formatShotList(doc: unknown): string | null {
|
|
13
|
+
const d = doc as AFPDocument;
|
|
14
|
+
if (!VIDEO_CLASSES.has(String(d['class']))) return null;
|
|
15
|
+
|
|
16
|
+
const news = Array.isArray(d.news) ? d.news : [];
|
|
17
|
+
const shots = parseShotList(news.join('\n'));
|
|
18
|
+
if (shots.length === 0) return null;
|
|
19
|
+
|
|
20
|
+
const lines = shots.map((shot) => {
|
|
21
|
+
const head = `${shot.numero}. [${shot.start}-${shot.end}] ${shot.description}`.trimEnd();
|
|
22
|
+
const quotes = shot.citations.map((c) => ` "${c.text}"`);
|
|
23
|
+
return [head, ...quotes].join('\n');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return `## Shot list\n\n${lines.join('\n')}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
4
29
|
export function truncationHint(remaining?: number): string {
|
|
5
30
|
const suffix = remaining != null && remaining > 0
|
|
6
31
|
? ` ${remaining} additional item(s) not returned.`
|
|
@@ -163,7 +188,7 @@ export function formatFullArticle(doc: unknown): TextContent {
|
|
|
163
188
|
if (flags) lines.push(flags);
|
|
164
189
|
|
|
165
190
|
const meta = lines.join('\n');
|
|
166
|
-
const body = (Array.isArray(d.news) ? d.news : []).join('\n\n');
|
|
191
|
+
const body = formatShotList(d) ?? (Array.isArray(d.news) ? d.news : []).join('\n\n');
|
|
167
192
|
|
|
168
193
|
return textContent(`## ${d.headline}\n\n${meta}\n\n---\n\n${body}`);
|
|
169
194
|
}
|