@manudota/artist-mcp 2.3.1 → 2.3.2-staging.116
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/dist/dispatch.js +3 -1
- package/dist/notes.d.ts +16 -1
- package/dist/notes.js +28 -4
- package/dist/server.d.ts +39 -0
- package/dist/server.js +177 -10
- package/package.json +1 -1
package/dist/dispatch.js
CHANGED
|
@@ -115,7 +115,9 @@ export const dispatchWith = (resolve, record = recordWrite) => async (op, params
|
|
|
115
115
|
case 'list_notebooks':
|
|
116
116
|
return (await listNotebooks(token));
|
|
117
117
|
case 'list_notes':
|
|
118
|
-
return (await listNotes(token
|
|
118
|
+
return (await listNotes(token, {
|
|
119
|
+
section: typeof params.section === 'string' ? params.section : undefined,
|
|
120
|
+
}));
|
|
119
121
|
case 'map_notes':
|
|
120
122
|
// The pages are chosen by the caller, which is where the notebook scope
|
|
121
123
|
// is settled; nothing here maps a notebook it was not given.
|
package/dist/notes.d.ts
CHANGED
|
@@ -118,11 +118,26 @@ export declare const listNotebooks: (token: string) => Promise<{
|
|
|
118
118
|
*
|
|
119
119
|
* Do not "simplify" this back to the single call.
|
|
120
120
|
*/
|
|
121
|
-
|
|
121
|
+
/** How a section name is compared: case and whitespace runs ignored, nothing else. */
|
|
122
|
+
export declare const sectionKey: (name: string) => string;
|
|
123
|
+
/** Graph caps a page listing here, and the walk does not page past it. See #178. */
|
|
124
|
+
export declare const PAGE_LISTING_CAP = 100;
|
|
125
|
+
export declare const listNotes: (token: string, { section }?: {
|
|
126
|
+
section?: string;
|
|
127
|
+
}) => Promise<{
|
|
122
128
|
notes: NoteSummary[];
|
|
123
129
|
sections: SectionSummary[];
|
|
124
130
|
/** Whether `last_modified` on every page is really its creation date. */
|
|
125
131
|
page_dates_are_creation_dates: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Every section on the account, by name, when `section` narrowed the walk.
|
|
134
|
+
* The pages of the others were never fetched, so they carry no count — but a
|
|
135
|
+
* name that matched nothing still needs something to be compared against.
|
|
136
|
+
*/
|
|
137
|
+
all_sections?: {
|
|
138
|
+
name: string;
|
|
139
|
+
notebook: string | null;
|
|
140
|
+
}[];
|
|
126
141
|
}>;
|
|
127
142
|
/**
|
|
128
143
|
* The sections that changed within a window.
|
package/dist/notes.js
CHANGED
|
@@ -165,7 +165,11 @@ export const listNotebooks = async (token) => {
|
|
|
165
165
|
*
|
|
166
166
|
* Do not "simplify" this back to the single call.
|
|
167
167
|
*/
|
|
168
|
-
|
|
168
|
+
/** How a section name is compared: case and whitespace runs ignored, nothing else. */
|
|
169
|
+
export const sectionKey = (name) => name.trim().replace(/\s+/g, ' ').toLowerCase();
|
|
170
|
+
/** Graph caps a page listing here, and the walk does not page past it. See #178. */
|
|
171
|
+
export const PAGE_LISTING_CAP = 100;
|
|
172
|
+
export const listNotes = async (token, { section } = {}) => {
|
|
169
173
|
const startedAt = Date.now();
|
|
170
174
|
let sectionCount = 0;
|
|
171
175
|
let failure = null;
|
|
@@ -177,13 +181,22 @@ export const listNotes = async (token) => {
|
|
|
177
181
|
'/me/onenote/sections?$select=id,displayName,lastModifiedDateTime' +
|
|
178
182
|
'&$expand=parentNotebook($select=displayName)&$top=100', token);
|
|
179
183
|
const sections = (await sectionsRes.json()).value ?? [];
|
|
180
|
-
const
|
|
184
|
+
const valid = sections.filter((s) => typeof s.id === 'string' && ONENOTE_ID.test(s.id));
|
|
185
|
+
// Narrowed before the fan-out, which is the whole saving: one section's pages
|
|
186
|
+
// instead of every section's. Matched on the whole name, not a substring —
|
|
187
|
+
// "BCW" would otherwise walk every BCW project and hand back a page from the
|
|
188
|
+
// wrong one, and the update flow needs exactly one page.
|
|
189
|
+
//
|
|
190
|
+
// Runs of whitespace count as one space: a real section is named
|
|
191
|
+
// "BCW Klagenfurt 03.07.2027 Vidala", and nobody types the second space.
|
|
192
|
+
const wanted = section === undefined ? undefined : sectionKey(section);
|
|
193
|
+
const usable = wanted === undefined ? valid : valid.filter((s) => sectionKey(s.displayName ?? '') === wanted);
|
|
181
194
|
sectionCount = usable.length;
|
|
182
195
|
const perSection = await mapWithConcurrency(usable, FANOUT_LIMIT, async (section) => {
|
|
183
196
|
const res = await graphGet(`/me/onenote/sections/${section.id}/pages` +
|
|
184
197
|
// createdDateTime is selected only so the two can be compared. It
|
|
185
198
|
// costs nothing — the request is made either way.
|
|
186
|
-
|
|
199
|
+
`?$select=id,title,createdDateTime,lastModifiedDateTime&$top=${PAGE_LISTING_CAP}`, token);
|
|
187
200
|
const pages = (await res.json()).value ?? [];
|
|
188
201
|
return pages.map((p) => ({
|
|
189
202
|
id: p.id,
|
|
@@ -210,7 +223,18 @@ export const listNotes = async (token) => {
|
|
|
210
223
|
// Sections newest first for the same reason, and because when page dates are
|
|
211
224
|
// useless this is the ordering the caller actually reads.
|
|
212
225
|
const sectionList = sectionSummaries.sort((a, b) => (b.last_modified ?? '').localeCompare(a.last_modified ?? ''));
|
|
213
|
-
|
|
226
|
+
if (wanted === undefined) {
|
|
227
|
+
return { notes, sections: sectionList, page_dates_are_creation_dates: creationDates };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
notes,
|
|
231
|
+
sections: sectionList,
|
|
232
|
+
page_dates_are_creation_dates: creationDates,
|
|
233
|
+
all_sections: valid.map((s) => ({
|
|
234
|
+
name: s.displayName ?? '(unnamed section)',
|
|
235
|
+
notebook: s.parentNotebook?.displayName ?? null,
|
|
236
|
+
})),
|
|
237
|
+
};
|
|
214
238
|
}
|
|
215
239
|
catch (err) {
|
|
216
240
|
failure = err instanceof Error ? err.message : String(err);
|
package/dist/server.d.ts
CHANGED
|
@@ -10,6 +10,14 @@ import { type Operation } from "./dispatch.js";
|
|
|
10
10
|
* that is the point — there is one set of them, not two that drift.
|
|
11
11
|
*/
|
|
12
12
|
type Dispatch = <T>(op: Operation, params?: Record<string, unknown>) => Promise<T>;
|
|
13
|
+
type NoteSummary = {
|
|
14
|
+
id: string;
|
|
15
|
+
title: string;
|
|
16
|
+
section: string | null;
|
|
17
|
+
/** Absent from responses served by an older edge function. */
|
|
18
|
+
notebook?: string | null;
|
|
19
|
+
last_modified: string | null;
|
|
20
|
+
};
|
|
13
21
|
type AttachmentBody = {
|
|
14
22
|
filename: string;
|
|
15
23
|
mime_type: string;
|
|
@@ -58,6 +66,37 @@ type AttachmentMap = {
|
|
|
58
66
|
}[];
|
|
59
67
|
note: string | null;
|
|
60
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* The answer to a `section` that matched nothing, or matched more than one.
|
|
71
|
+
*
|
|
72
|
+
* Neither is allowed to fall through to a page list. The update flow resolves a
|
|
73
|
+
* chat message to exactly one page, and an empty list reads as "this project
|
|
74
|
+
* has no page" — which is a real finding for a section like BCW Megeve and a
|
|
75
|
+
* false one for a typo. So the names are offered, closest first, and choosing
|
|
76
|
+
* is left to the user.
|
|
77
|
+
*/
|
|
78
|
+
export declare const renderSectionMiss: (section: string, matched: {
|
|
79
|
+
name: string;
|
|
80
|
+
notebook: string | null;
|
|
81
|
+
}[], all: {
|
|
82
|
+
name: string;
|
|
83
|
+
notebook: string | null;
|
|
84
|
+
}[],
|
|
85
|
+
/** Set when the search spanned every notebook: names carry their notebook, and the key to choose one. */
|
|
86
|
+
notebookKey?: string) => string | null;
|
|
87
|
+
/**
|
|
88
|
+
* Which page in a resolved section an update belongs to (#193).
|
|
89
|
+
*
|
|
90
|
+
* Matched on the title's start, not the whole title: the live notebook names
|
|
91
|
+
* them `CL Aufgaben — Montepulciano`, plain `CL Aufgaben`, and
|
|
92
|
+
* `CL Aufgaben — Melk BCW (Barocktage 2027)`, and a whole-title convention
|
|
93
|
+
* would miss two of three. Computed from every page in the section, before
|
|
94
|
+
* `limit` or `since` trim the list, so a capped listing cannot hide it.
|
|
95
|
+
*
|
|
96
|
+
* None and several are both said outright. None is a real finding — the update
|
|
97
|
+
* has nowhere to go, and it must not land on a neighbouring page instead.
|
|
98
|
+
*/
|
|
99
|
+
export declare const renderUpdateTarget: (pages: readonly NoteSummary[]) => string;
|
|
61
100
|
/**
|
|
62
101
|
* Compose the briefing `list_agent_workflows` returns.
|
|
63
102
|
*
|
package/dist/server.js
CHANGED
|
@@ -6,7 +6,7 @@ import { WRITE_CAPABILITIES, isGranted } from "./grants.js";
|
|
|
6
6
|
import { listAgentWorkflows, loadAgentWorkflow } from "./agents.js";
|
|
7
7
|
import { GraphError } from "./client.js";
|
|
8
8
|
import { call as localCall } from "./dispatch.js";
|
|
9
|
-
import { narrowNotes, narrowSections, notebookKeyFor } from "./notes.js";
|
|
9
|
+
import { PAGE_LISTING_CAP, narrowNotes, narrowSections, notebookKeyFor, sectionKey } from "./notes.js";
|
|
10
10
|
/** One call per page, so a notebook nobody put a number on does not become hundreds of requests. */
|
|
11
11
|
const DEFAULT_MAP_PAGES = 40;
|
|
12
12
|
/** Times are stated with their zone; the calendar's zone need not be the reader's. */
|
|
@@ -34,7 +34,7 @@ const describeSize = (bytes) => {
|
|
|
34
34
|
* intake depends on, and two copies of it would eventually disagree. The caller
|
|
35
35
|
* names itself so the instruction says which tool to call again.
|
|
36
36
|
*/
|
|
37
|
-
const selectNotebook = async (call, notebook, notebookKey, tool) => {
|
|
37
|
+
const selectNotebook = async (call, notebook, notebookKey, tool, section) => {
|
|
38
38
|
// The notebook question first, and on its own, because it is the cheap one.
|
|
39
39
|
//
|
|
40
40
|
// `list_notes` answers it as a side effect of fetching every page of every
|
|
@@ -87,7 +87,7 @@ const selectNotebook = async (call, notebook, notebookKey, tool) => {
|
|
|
87
87
|
return { message: `No notebook named "${notebook}". Available: ${names.join(", ")}.` };
|
|
88
88
|
}
|
|
89
89
|
// Settled. Only now are the pages worth what they cost.
|
|
90
|
-
const { notes, sections = [], page_dates_are_creation_dates = false, } = await call("list_notes");
|
|
90
|
+
const { notes, sections = [], page_dates_are_creation_dates = false, all_sections, } = await call("list_notes", section === undefined ? {} : { section });
|
|
91
91
|
const pages = wanted
|
|
92
92
|
? notes.filter((n) => (n.notebook ?? "").trim().toLowerCase() === wanted)
|
|
93
93
|
: notes;
|
|
@@ -115,6 +115,130 @@ const selectNotebook = async (call, notebook, notebookKey, tool) => {
|
|
|
115
115
|
sections: inScope,
|
|
116
116
|
creationDates: page_dates_are_creation_dates,
|
|
117
117
|
scope,
|
|
118
|
+
allSections: wanted
|
|
119
|
+
? all_sections?.filter((sec) => (sec.notebook ?? "").trim().toLowerCase() === wanted)
|
|
120
|
+
: all_sections,
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The answer to a `section` that matched nothing, or matched more than one.
|
|
125
|
+
*
|
|
126
|
+
* Neither is allowed to fall through to a page list. The update flow resolves a
|
|
127
|
+
* chat message to exactly one page, and an empty list reads as "this project
|
|
128
|
+
* has no page" — which is a real finding for a section like BCW Megeve and a
|
|
129
|
+
* false one for a typo. So the names are offered, closest first, and choosing
|
|
130
|
+
* is left to the user.
|
|
131
|
+
*/
|
|
132
|
+
export const renderSectionMiss = (section, matched, all,
|
|
133
|
+
/** Set when the search spanned every notebook: names carry their notebook, and the key to choose one. */
|
|
134
|
+
notebookKey) => {
|
|
135
|
+
if (matched.length === 1)
|
|
136
|
+
return null;
|
|
137
|
+
const label = (sec) => notebookKey === undefined ? sec.name : `${sec.name} (notebook: ${sec.notebook ?? "unknown"})`;
|
|
138
|
+
const callAgain = notebookKey === undefined
|
|
139
|
+
? "call list_notes again with that section."
|
|
140
|
+
: "call list_notes again with that section, its notebook, and the notebook_key below";
|
|
141
|
+
// On its own line, so no sentence punctuation is ever read as part of it.
|
|
142
|
+
const keyLine = notebookKey === undefined ? "" : `\n\nnotebook_key: ${notebookKey}`;
|
|
143
|
+
if (matched.length > 1) {
|
|
144
|
+
const where = matched.map((sec) => `- ${label(sec)}`);
|
|
145
|
+
return (`${matched.length} sections are named "${section}":\n${where.join("\n")}\n\n` +
|
|
146
|
+
`Ask the user which one they mean, then ${callAgain}. Do not pick one — ` +
|
|
147
|
+
"the same project name in two seasons is two different projects." +
|
|
148
|
+
keyLine);
|
|
149
|
+
}
|
|
150
|
+
// Scored by how many words are shared, and only the best score is offered.
|
|
151
|
+
// Against the real notebook, "any shared word" named every BCW section for a
|
|
152
|
+
// single BCW typo — a list that long is no closer than the full one.
|
|
153
|
+
const needle = sectionKey(section);
|
|
154
|
+
const words = needle.split(" ").filter((w) => w.length > 2);
|
|
155
|
+
const score = (name) => {
|
|
156
|
+
const hay = sectionKey(name);
|
|
157
|
+
if (hay.includes(needle) || needle.includes(hay))
|
|
158
|
+
return words.length + 1;
|
|
159
|
+
return words.filter((w) => hay.split(" ").includes(w)).length;
|
|
160
|
+
};
|
|
161
|
+
const scored = all.map((sec) => ({ sec, s: score(sec.name) }));
|
|
162
|
+
const best = Math.max(0, ...scored.map(({ s }) => s));
|
|
163
|
+
const near = scored.filter(({ s }) => best > 0 && s === best).map(({ sec }) => label(sec));
|
|
164
|
+
return (`No section is named "${section}". ` +
|
|
165
|
+
(near.length > 0
|
|
166
|
+
? `Closest: ${[...new Set(near)].join(", ")}. `
|
|
167
|
+
: `Sections: ${[...new Set(all.map(label))].join(", ")}. `) +
|
|
168
|
+
`Ask the user which one they mean rather than choosing, then ${callAgain}. ` +
|
|
169
|
+
"Do not read this as the project having no page." +
|
|
170
|
+
keyLine);
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Which page in a resolved section an update belongs to (#193).
|
|
174
|
+
*
|
|
175
|
+
* Matched on the title's start, not the whole title: the live notebook names
|
|
176
|
+
* them `CL Aufgaben — Montepulciano`, plain `CL Aufgaben`, and
|
|
177
|
+
* `CL Aufgaben — Melk BCW (Barocktage 2027)`, and a whole-title convention
|
|
178
|
+
* would miss two of three. Computed from every page in the section, before
|
|
179
|
+
* `limit` or `since` trim the list, so a capped listing cannot hide it.
|
|
180
|
+
*
|
|
181
|
+
* None and several are both said outright. None is a real finding — the update
|
|
182
|
+
* has nowhere to go, and it must not land on a neighbouring page instead.
|
|
183
|
+
*/
|
|
184
|
+
export const renderUpdateTarget = (pages) => {
|
|
185
|
+
const targets = pages.filter((p) => sectionKey(p.title).startsWith("cl aufgaben"));
|
|
186
|
+
if (targets.length === 1) {
|
|
187
|
+
const [t] = targets;
|
|
188
|
+
return (`CL Aufgaben page in this section: "${t.title}" (id: ${t.id}). ` +
|
|
189
|
+
"An update to this project belongs on this page.");
|
|
190
|
+
}
|
|
191
|
+
if (targets.length === 0) {
|
|
192
|
+
return ("This section has no CL Aufgaben page, so an update to this project has " +
|
|
193
|
+
"no page to go to. Say so; do not write it onto another page in the section.");
|
|
194
|
+
}
|
|
195
|
+
return (`This section has ${targets.length} CL Aufgaben pages: ` +
|
|
196
|
+
targets.map((t) => `"${t.title}" (id: ${t.id})`).join(", ") +
|
|
197
|
+
". Ask the user which one an update belongs on. Do not pick one.");
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* `section` with no notebook, on an account holding several: the update flow's
|
|
201
|
+
* case, since "Melk is confirmed" names a project and never a season.
|
|
202
|
+
*
|
|
203
|
+
* Section NAMES are searched across every notebook — they arrive in the one
|
|
204
|
+
* sections call either way, so this costs nothing. PAGES still come back only
|
|
205
|
+
* when exactly one section in exactly one notebook matched, and the reply names
|
|
206
|
+
* that notebook so the answer cannot silently drift into the wrong season.
|
|
207
|
+
* Everything else — two seasons, a partial name — goes back to the user with
|
|
208
|
+
* the notebook_key, the same proof `selectNotebook` asks for.
|
|
209
|
+
*/
|
|
210
|
+
const findSectionAcrossNotebooks = async (call, section) => {
|
|
211
|
+
const { notebooks } = await call("list_notebooks");
|
|
212
|
+
// One notebook is not a choice; the ordinary path already handles it.
|
|
213
|
+
if (notebooks.length <= 1)
|
|
214
|
+
return null;
|
|
215
|
+
const names = notebooks.map((n) => n.name);
|
|
216
|
+
const { notes, sections = [], page_dates_are_creation_dates = false, all_sections = [], } = await call("list_notes", { section });
|
|
217
|
+
const miss = renderSectionMiss(section, sections, all_sections, notebookKeyFor(names));
|
|
218
|
+
if (miss !== null)
|
|
219
|
+
return { message: miss };
|
|
220
|
+
const [found] = sections;
|
|
221
|
+
// A unique exact match is not proof of the right season. Found live:
|
|
222
|
+
// "Montepulciano" resolved to 2026-27 while "Montepulciano 2028" sat in
|
|
223
|
+
// 2027-28, and an update naming the festival could mean either.
|
|
224
|
+
const wanted = sectionKey(section);
|
|
225
|
+
const similar = all_sections.filter((sec) => {
|
|
226
|
+
const key = sectionKey(sec.name);
|
|
227
|
+
return key !== wanted && (key.includes(wanted) || wanted.includes(key));
|
|
228
|
+
});
|
|
229
|
+
const others = names.filter((name) => name.trim().toLowerCase() !== (found.notebook ?? "").trim().toLowerCase());
|
|
230
|
+
return {
|
|
231
|
+
pages: notes,
|
|
232
|
+
sections,
|
|
233
|
+
creationDates: page_dates_are_creation_dates,
|
|
234
|
+
scope: `Found in notebook "${found.notebook ?? "unknown"}", the only notebook with a ` +
|
|
235
|
+
`section of this name (others: ${others.join(", ")}). Name that notebook when ` +
|
|
236
|
+
"you answer, so the user can catch a wrong season." +
|
|
237
|
+
(similar.length > 0
|
|
238
|
+
? " Similarly named, and possibly the project meant: " +
|
|
239
|
+
similar.map((sec) => `${sec.name} (notebook: ${sec.notebook ?? "unknown"})`).join(", ") +
|
|
240
|
+
". If the update could belong to one of these, ask before using this section."
|
|
241
|
+
: ""),
|
|
118
242
|
};
|
|
119
243
|
};
|
|
120
244
|
/**
|
|
@@ -176,7 +300,7 @@ const renderChangedSections = (chosen, notebook, since) => {
|
|
|
176
300
|
* the only way OneNote allows a cell to change.
|
|
177
301
|
*/
|
|
178
302
|
const INDEX_ENTRIES = 12;
|
|
179
|
-
const serverVersion = '2.3.
|
|
303
|
+
const serverVersion = '2.3.2-staging.116'; // x-release-please-version
|
|
180
304
|
const errorResult = (err) => {
|
|
181
305
|
const message = err instanceof GraphError ? err.message : `Unexpected error: ${err}`;
|
|
182
306
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -349,10 +473,19 @@ const renderWorkflowBriefing = async (entries, load, writes = []) => {
|
|
|
349
473
|
"install time. Everything not listed here remains read-only, " +
|
|
350
474
|
"including all of OneNote.",
|
|
351
475
|
...writes.map((name) => `- ${name}: ${WRITE_CAPABILITIES[name]}`),
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
476
|
+
// Recording a dispute is not settling one. The CL Aufgaben pages
|
|
477
|
+
// carry a convention for it — the field becomes UNGEKLÄRT and both
|
|
478
|
+
// values go under "Widersprüchliche Angaben" — and the earlier
|
|
479
|
+
// wording ("may never be written") read as forbidding that too, in
|
|
480
|
+
// the one surface that outranks the playbook saying to do it (#193).
|
|
481
|
+
"A disputed or UNKNOWN value may never be written as though it were " +
|
|
482
|
+
"settled. If two sources disagree, or a field is unsettled, never " +
|
|
483
|
+
"write one side as the value — a written value persists and other " +
|
|
484
|
+
"people see it, which is exactly the decision policy:divergence " +
|
|
485
|
+
"refuses to make. Recording the dispute itself is allowed where the " +
|
|
486
|
+
"page has a place for it: both values with their origins, and the " +
|
|
487
|
+
"field left UNGEKLÄRT or UNKNOWN. Where it has no such place, write " +
|
|
488
|
+
"nothing for that field and say why.",
|
|
356
489
|
];
|
|
357
490
|
return [
|
|
358
491
|
...alarm,
|
|
@@ -718,6 +851,14 @@ const createServer = async (call, grants = []) => {
|
|
|
718
851
|
"notebook you happen to know the user has: a plausible guess here " +
|
|
719
852
|
"is indistinguishable from their choice and produces a confident " +
|
|
720
853
|
"answer about the wrong notebook."),
|
|
854
|
+
section: z
|
|
855
|
+
.string()
|
|
856
|
+
.optional()
|
|
857
|
+
.describe("Exact name of one section, to list only its pages. Much cheaper " +
|
|
858
|
+
"than the whole notebook, and the reply gives the section's full " +
|
|
859
|
+
"page count. Use it to find the one page an update belongs to. " +
|
|
860
|
+
"Works without `notebook`: the section is then looked for in every " +
|
|
861
|
+
"notebook, and the reply says which one it was found in."),
|
|
721
862
|
notebook_key: z
|
|
722
863
|
.string()
|
|
723
864
|
.optional()
|
|
@@ -745,12 +886,20 @@ const createServer = async (call, grants = []) => {
|
|
|
745
886
|
.describe("Cap the number of pages returned, newest first. The reply says how " +
|
|
746
887
|
"many matched, so a capped list is never mistaken for the whole " +
|
|
747
888
|
"notebook."),
|
|
748
|
-
}, async ({ notebook, notebook_key, since, limit }) => {
|
|
889
|
+
}, async ({ notebook, notebook_key, section, since, limit }) => {
|
|
749
890
|
try {
|
|
750
|
-
const chosen =
|
|
891
|
+
const chosen = (section !== undefined && notebook === undefined
|
|
892
|
+
? await findSectionAcrossNotebooks(call, section)
|
|
893
|
+
: null) ??
|
|
894
|
+
(await selectNotebook(call, notebook, notebook_key, "list_notes", section));
|
|
751
895
|
if ("message" in chosen) {
|
|
752
896
|
return { content: [{ type: "text", text: chosen.message }] };
|
|
753
897
|
}
|
|
898
|
+
if (section !== undefined) {
|
|
899
|
+
const miss = renderSectionMiss(section, chosen.sections, chosen.allSections ?? []);
|
|
900
|
+
if (miss !== null)
|
|
901
|
+
return { content: [{ type: "text", text: miss }] };
|
|
902
|
+
}
|
|
754
903
|
const selected = chosen.pages;
|
|
755
904
|
// When page dates carry no modification information, a `since` question
|
|
756
905
|
// is answered about SECTIONS instead — and answers with sections, not
|
|
@@ -774,6 +923,13 @@ const createServer = async (call, grants = []) => {
|
|
|
774
923
|
// Narrowed only after the notebook is settled, so a `since` window can
|
|
775
924
|
// never be what makes a notebook look empty enough to skip choosing.
|
|
776
925
|
const { notes: shown, matched, undated } = narrowNotes(selected, { since, limit });
|
|
926
|
+
if (shown.length === 0 && section !== undefined && since === undefined) {
|
|
927
|
+
return {
|
|
928
|
+
content: [
|
|
929
|
+
{ type: "text", text: `Section "${chosen.sections[0].name}" holds no pages.` },
|
|
930
|
+
],
|
|
931
|
+
};
|
|
932
|
+
}
|
|
777
933
|
if (shown.length === 0) {
|
|
778
934
|
const scope = notebook ? `"${notebook}"` : "this account";
|
|
779
935
|
return {
|
|
@@ -805,6 +961,17 @@ const createServer = async (call, grants = []) => {
|
|
|
805
961
|
const caveats = [];
|
|
806
962
|
if (chosen.scope)
|
|
807
963
|
caveats.push(chosen.scope);
|
|
964
|
+
if (section !== undefined) {
|
|
965
|
+
caveats.push(renderUpdateTarget(chosen.pages));
|
|
966
|
+
const [sec] = chosen.sections;
|
|
967
|
+
// Stated whether or not it is short, because "has this page seen
|
|
968
|
+
// everything in its section" is answered against this number (#193).
|
|
969
|
+
caveats.push(sec.pages >= PAGE_LISTING_CAP
|
|
970
|
+
? `Section "${sec.name}" returned ${sec.pages} pages, which is the ` +
|
|
971
|
+
"listing cap: there may be more that were not fetched (#178)."
|
|
972
|
+
: `Section "${sec.name}" holds ${sec.pages} page${sec.pages === 1 ? "" : "s"}; ` +
|
|
973
|
+
"this is all of them.");
|
|
974
|
+
}
|
|
808
975
|
if (shown.length < matched) {
|
|
809
976
|
caveats.push(`Showing the ${shown.length} newest by date of ${matched} matching ` +
|
|
810
977
|
"pages. Raise `limit` or narrow with `since` for the rest.");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manudota/artist-mcp",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2-staging.116",
|
|
4
4
|
"description": "MCP server that reads your OneNote notes in Claude Desktop or Codex, with optional Gmail and Calendar as supporting evidence. Signs in on your own machine; writes are opt-in per install.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|