@manudota/artist-mcp 2.2.2-staging.103 → 2.3.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/dist/agents.js +20 -1
- package/dist/api.d.ts +23 -0
- package/dist/api.js +36 -0
- package/dist/dispatch.d.ts +4 -0
- package/dist/dispatch.js +4 -1
- package/dist/mail.js +3 -3
- package/dist/notes.d.ts +71 -4
- package/dist/notes.js +233 -94
- package/dist/onenote-patch.js +27 -0
- package/dist/server.js +74 -45
- package/package.json +1 -1
package/dist/agents.js
CHANGED
|
@@ -117,7 +117,26 @@ const resolveRegistry = async () => {
|
|
|
117
117
|
]));
|
|
118
118
|
const localRoot = localAgentRoot();
|
|
119
119
|
if (localRoot) {
|
|
120
|
-
|
|
120
|
+
const local = (await readLocalRegistry(localRoot)).entries;
|
|
121
|
+
// Replacing rather than layering, where the deployment says so.
|
|
122
|
+
//
|
|
123
|
+
// Layering is right for a musician who has copied out a few playbooks and
|
|
124
|
+
// edited them: the ones they did not copy should keep working. Hosted is
|
|
125
|
+
// the other case — one pack, written for one notebook, and a bundled
|
|
126
|
+
// playbook surviving because the custom pack happens not to name its id is
|
|
127
|
+
// a generic rule in force that nobody chose. Cleared before the local
|
|
128
|
+
// entries go in, so what is served is exactly what is in that directory.
|
|
129
|
+
//
|
|
130
|
+
// Only ever narrows: a mode this does not recognise layers, which is what
|
|
131
|
+
// every install that has never heard of it already does.
|
|
132
|
+
// No emptiness check here, deliberately. `readLocalRegistry` already throws
|
|
133
|
+
// for a directory with no `artist/` and for one holding no Markdown, so by
|
|
134
|
+
// this line `local` cannot be empty and a guard would be unreachable code
|
|
135
|
+
// claiming to protect something. The strictness this mode needs is the
|
|
136
|
+
// strictness the local layer already has.
|
|
137
|
+
if (process.env.ARTIST_MCP_PACK_MODE === 'replace')
|
|
138
|
+
byId.clear();
|
|
139
|
+
for (const entry of local) {
|
|
121
140
|
byId.set(entry.id, { ...entry, source: 'local', origin: localRoot });
|
|
122
141
|
}
|
|
123
142
|
}
|
package/dist/api.d.ts
CHANGED
|
@@ -9,6 +9,29 @@
|
|
|
9
9
|
export declare const GRAPH = "https://graph.microsoft.com/v1.0";
|
|
10
10
|
export declare const GMAIL = "https://gmail.googleapis.com/gmail/v1";
|
|
11
11
|
export declare const CALENDAR = "https://www.googleapis.com/calendar/v3";
|
|
12
|
+
/**
|
|
13
|
+
* How many provider requests one tool call may have in flight at once.
|
|
14
|
+
*
|
|
15
|
+
* Jitter above spreads the *retries* of a herd; nothing spread the herd's
|
|
16
|
+
* first arrival, and that is the burst the logs actually show — forty
|
|
17
|
+
* `attempt=1 waited=0s` refusals landing in the same millisecond, because a
|
|
18
|
+
* survey of a forty-page notebook opened forty Graph requests simultaneously.
|
|
19
|
+
* Every one of them then had to win its own retry ladder against a provider
|
|
20
|
+
* that was only busy because of them.
|
|
21
|
+
*
|
|
22
|
+
* Four is chosen to be boring rather than optimal: it is slow enough that
|
|
23
|
+
* OneNote answers instead of refusing, and parallel enough that a survey is
|
|
24
|
+
* still meaningfully faster than a loop. The number that matters is "not N".
|
|
25
|
+
*/
|
|
26
|
+
export declare const FANOUT_LIMIT = 4;
|
|
27
|
+
/**
|
|
28
|
+
* `Promise.all(items.map(fn))` with a ceiling on how many run at once.
|
|
29
|
+
*
|
|
30
|
+
* Results stay in the order of `items`, and a rejection rejects the whole
|
|
31
|
+
* call, so this is a drop-in for the `Promise.all` it replaces: callers that
|
|
32
|
+
* already catch per item keep behaving exactly as they did.
|
|
33
|
+
*/
|
|
34
|
+
export declare const mapWithConcurrency: <T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>) => Promise<R[]>;
|
|
12
35
|
/**
|
|
13
36
|
* What a call needs from the grant, for the one failure that is not a fault.
|
|
14
37
|
*
|
package/dist/api.js
CHANGED
|
@@ -47,6 +47,42 @@ const THROTTLE_BUDGET_MS = 8_000;
|
|
|
47
47
|
* arrivals scattered across the window.
|
|
48
48
|
*/
|
|
49
49
|
const jittered = (ms) => Math.round(ms / 2 + Math.random() * (ms / 2));
|
|
50
|
+
/**
|
|
51
|
+
* How many provider requests one tool call may have in flight at once.
|
|
52
|
+
*
|
|
53
|
+
* Jitter above spreads the *retries* of a herd; nothing spread the herd's
|
|
54
|
+
* first arrival, and that is the burst the logs actually show — forty
|
|
55
|
+
* `attempt=1 waited=0s` refusals landing in the same millisecond, because a
|
|
56
|
+
* survey of a forty-page notebook opened forty Graph requests simultaneously.
|
|
57
|
+
* Every one of them then had to win its own retry ladder against a provider
|
|
58
|
+
* that was only busy because of them.
|
|
59
|
+
*
|
|
60
|
+
* Four is chosen to be boring rather than optimal: it is slow enough that
|
|
61
|
+
* OneNote answers instead of refusing, and parallel enough that a survey is
|
|
62
|
+
* still meaningfully faster than a loop. The number that matters is "not N".
|
|
63
|
+
*/
|
|
64
|
+
export const FANOUT_LIMIT = 4;
|
|
65
|
+
/**
|
|
66
|
+
* `Promise.all(items.map(fn))` with a ceiling on how many run at once.
|
|
67
|
+
*
|
|
68
|
+
* Results stay in the order of `items`, and a rejection rejects the whole
|
|
69
|
+
* call, so this is a drop-in for the `Promise.all` it replaces: callers that
|
|
70
|
+
* already catch per item keep behaving exactly as they did.
|
|
71
|
+
*/
|
|
72
|
+
export const mapWithConcurrency = async (items, limit, fn) => {
|
|
73
|
+
const results = new Array(items.length);
|
|
74
|
+
let next = 0;
|
|
75
|
+
const worker = async () => {
|
|
76
|
+
for (;;) {
|
|
77
|
+
const index = next++;
|
|
78
|
+
if (index >= items.length)
|
|
79
|
+
return;
|
|
80
|
+
results[index] = await fn(items[index], index);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
|
|
84
|
+
return results;
|
|
85
|
+
};
|
|
50
86
|
/**
|
|
51
87
|
* What the provider actually asked for, when it says.
|
|
52
88
|
*
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -30,6 +30,10 @@ import { type ProviderName } from './tokens.js';
|
|
|
30
30
|
* fails on any edit to this table so that the change has to be deliberate.
|
|
31
31
|
*/
|
|
32
32
|
export declare const OPERATIONS: {
|
|
33
|
+
readonly list_notebooks: {
|
|
34
|
+
readonly provider: "microsoft";
|
|
35
|
+
readonly effect: "read";
|
|
36
|
+
};
|
|
33
37
|
readonly list_notes: {
|
|
34
38
|
readonly provider: "microsoft";
|
|
35
39
|
readonly effect: "read";
|
package/dist/dispatch.js
CHANGED
|
@@ -16,7 +16,7 @@ import { gmailLoader, mapAttachment, readAttachment } from './attachments.js';
|
|
|
16
16
|
import { oneNoteLoader } from './page-attachments.js';
|
|
17
17
|
import { createEvent, deleteEvent, listCalendars, listEvents, previewDeleteEvent, previewEvent, previewRescheduleEvent, readEvent, rescheduleEvent, } from './calendar.js';
|
|
18
18
|
import { listEmails, readEmail } from './mail.js';
|
|
19
|
-
import { listNotes, mapNotes, readNote } from './notes.js';
|
|
19
|
+
import { listNotebooks, listNotes, mapNotes, readNote } from './notes.js';
|
|
20
20
|
import { applyEdit, previewEdit } from './onenote-patch.js';
|
|
21
21
|
import { createPage, previewPage } from './onenote-write.js';
|
|
22
22
|
import { accessTokenFor } from './oauth.js';
|
|
@@ -39,6 +39,7 @@ import { loadTokens } from './tokens.js';
|
|
|
39
39
|
* fails on any edit to this table so that the change has to be deliberate.
|
|
40
40
|
*/
|
|
41
41
|
export const OPERATIONS = {
|
|
42
|
+
list_notebooks: { provider: 'microsoft', effect: 'read' },
|
|
42
43
|
list_notes: { provider: 'microsoft', effect: 'read' },
|
|
43
44
|
map_notes: { provider: 'microsoft', effect: 'read' },
|
|
44
45
|
read_note: { provider: 'microsoft', effect: 'read' },
|
|
@@ -111,6 +112,8 @@ export const connectedProviders = async () => {
|
|
|
111
112
|
export const dispatchWith = (resolve, record = recordWrite) => async (op, params = {}) => {
|
|
112
113
|
const token = await resolve(PROVIDER_FOR[op]);
|
|
113
114
|
switch (op) {
|
|
115
|
+
case 'list_notebooks':
|
|
116
|
+
return (await listNotebooks(token));
|
|
114
117
|
case 'list_notes':
|
|
115
118
|
return (await listNotes(token));
|
|
116
119
|
case 'map_notes':
|
package/dist/mail.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* thing being worked on. Ported from the edge function with behaviour intact.
|
|
6
6
|
*/
|
|
7
7
|
import { GraphError } from './client.js';
|
|
8
|
-
import { gmailGet } from './api.js';
|
|
8
|
+
import { FANOUT_LIMIT, gmailGet, mapWithConcurrency } from './api.js';
|
|
9
9
|
import { htmlToText } from './notes.js';
|
|
10
10
|
/** Gmail ids are opaque hex-ish strings; anything else never reaches a URL. */
|
|
11
11
|
export const GMAIL_ID = /^[A-Za-z0-9_-]{1,128}$/;
|
|
@@ -119,7 +119,7 @@ export const listEmails = async (token, rawQuery) => {
|
|
|
119
119
|
const ids = ((await listRes.json()).messages ?? [])
|
|
120
120
|
.map((m) => m.id)
|
|
121
121
|
.filter((id) => typeof id === 'string' && GMAIL_ID.test(id));
|
|
122
|
-
const emails = await
|
|
122
|
+
const emails = await mapWithConcurrency(ids, FANOUT_LIMIT, async (id) => {
|
|
123
123
|
const res = await gmailGet(`/users/me/messages/${encodeURIComponent(id)}` +
|
|
124
124
|
'?format=metadata&metadataHeaders=Subject&metadataHeaders=From' +
|
|
125
125
|
'&metadataHeaders=To&metadataHeaders=Date', token);
|
|
@@ -133,7 +133,7 @@ export const listEmails = async (token, rawQuery) => {
|
|
|
133
133
|
date: header(msg.payload?.headers, 'Date'),
|
|
134
134
|
snippet: msg.snippet ?? null,
|
|
135
135
|
};
|
|
136
|
-
})
|
|
136
|
+
});
|
|
137
137
|
return { emails };
|
|
138
138
|
};
|
|
139
139
|
export const readEmail = async (token, emailId) => {
|
package/dist/notes.d.ts
CHANGED
|
@@ -8,6 +8,25 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { type PageResource } from './page-attachments.js';
|
|
10
10
|
import { type EditablePart } from './onenote-patch.js';
|
|
11
|
+
/**
|
|
12
|
+
* A short value proving a caller has actually seen this account's notebook list.
|
|
13
|
+
*
|
|
14
|
+
* The gate it serves used to be a module-level boolean — "has anything listed
|
|
15
|
+
* the notebooks yet" — which is a fair approximation of one CLI session and
|
|
16
|
+
* nonsense on hosted, where every request may land on a different instance and
|
|
17
|
+
* the flag is shared by every user on the one it lands on. It refused real
|
|
18
|
+
* selections on a cold instance and admitted unseen ones on a warm one.
|
|
19
|
+
*
|
|
20
|
+
* Derived from the notebook names rather than stored, so it needs no session
|
|
21
|
+
* and no shared storage: the same account yields the same key on any instance,
|
|
22
|
+
* a client that never saw the list cannot produce it, and a key from an account
|
|
23
|
+
* with different notebooks does not match. It is not a secret and does not need
|
|
24
|
+
* to be — it proves the list was seen, nothing more.
|
|
25
|
+
*
|
|
26
|
+
* Order and case are normalised so the key does not change when Graph returns
|
|
27
|
+
* the same notebooks in a different order.
|
|
28
|
+
*/
|
|
29
|
+
export declare const notebookKeyFor: (names: readonly string[]) => string;
|
|
11
30
|
export type NoteSummary = {
|
|
12
31
|
id: string;
|
|
13
32
|
title: string;
|
|
@@ -65,6 +84,31 @@ export declare const pageDatesAreCreationDates: (sections: readonly {
|
|
|
65
84
|
* both are marked.
|
|
66
85
|
*/
|
|
67
86
|
export declare const htmlToText: (html: string) => string;
|
|
87
|
+
/**
|
|
88
|
+
* Which notebooks this account has, and nothing else.
|
|
89
|
+
*
|
|
90
|
+
* One Graph request. `listNotes` answers the same question as a side effect of
|
|
91
|
+
* fetching every page of every section — a hundred requests on an organised
|
|
92
|
+
* account — and the notebook question is asked far more often than the page
|
|
93
|
+
* one: every call that has to ask "which notebook?" paid for the pages of all
|
|
94
|
+
* of them to print a list of names.
|
|
95
|
+
*
|
|
96
|
+
* That is what was throttling the account. Graph refused with 20166, "the app
|
|
97
|
+
* has issued too many requests on behalf of this user", against a listing whose
|
|
98
|
+
* answer was already contained in the single request that starts it, since
|
|
99
|
+
* `parentNotebook` expands inline on the sections call.
|
|
100
|
+
*
|
|
101
|
+
* Counted in sections rather than pages, because a page count is exactly the
|
|
102
|
+
* thing that cannot be known without the hundred requests. A number that costs
|
|
103
|
+
* a hundred requests to be slightly more familiar is not worth it, and the
|
|
104
|
+
* count is only there to help the user recognise which notebook is which.
|
|
105
|
+
*/
|
|
106
|
+
export declare const listNotebooks: (token: string) => Promise<{
|
|
107
|
+
notebooks: {
|
|
108
|
+
name: string;
|
|
109
|
+
sections: number;
|
|
110
|
+
}[];
|
|
111
|
+
}>;
|
|
68
112
|
/**
|
|
69
113
|
* `/me/onenote/pages` looks like the obvious call and works right up until the
|
|
70
114
|
* account has too many sections, at which point Graph fails the whole request
|
|
@@ -140,6 +184,31 @@ export type NoteSketch = NoteSummary & {
|
|
|
140
184
|
chars_total: number | null;
|
|
141
185
|
error: string | null;
|
|
142
186
|
};
|
|
187
|
+
/**
|
|
188
|
+
* How long a survey may spend before it returns what it has.
|
|
189
|
+
*
|
|
190
|
+
* The hosted route dies at sixty seconds (`maxDuration` in the MCP route), and
|
|
191
|
+
* dying there is the worst available outcome: the caller waits the full minute
|
|
192
|
+
* and receives nothing, not even the pages that were already sketched. Three
|
|
193
|
+
* of those in a row is what a chat client's retries look like from the logs.
|
|
194
|
+
*
|
|
195
|
+
* Forty-five leaves room for the answer to be assembled and sent after the
|
|
196
|
+
* last sketch lands. The number is a floor on usefulness rather than a tuning
|
|
197
|
+
* knob: whatever the notebook looks like, the survey now returns.
|
|
198
|
+
*/
|
|
199
|
+
export declare const MAP_DEADLINE_MS = 45000;
|
|
200
|
+
type MapNotesOptions = {
|
|
201
|
+
deadlineMs?: number;
|
|
202
|
+
/** Injectable so a test can reach the deadline without spending the time. */
|
|
203
|
+
now?: () => number;
|
|
204
|
+
};
|
|
205
|
+
export type MapNotesResult = {
|
|
206
|
+
sketches: NoteSketch[];
|
|
207
|
+
read_in_full: number;
|
|
208
|
+
/** Pages the deadline cut off. Unsurveyed, and reported as such. */
|
|
209
|
+
not_reached: number;
|
|
210
|
+
elapsed_ms: number;
|
|
211
|
+
};
|
|
143
212
|
/**
|
|
144
213
|
* Sketch every page in a chosen notebook, cheaply, so a notebook can be triaged
|
|
145
214
|
* without reading all of it.
|
|
@@ -156,10 +225,7 @@ export type NoteSketch = NoteSummary & {
|
|
|
156
225
|
*
|
|
157
226
|
* Scoped to one notebook, as intake requires. Nothing maps across scopes.
|
|
158
227
|
*/
|
|
159
|
-
export declare const mapNotes: (token: string, pages: NoteSummary[]) => Promise<
|
|
160
|
-
sketches: NoteSketch[];
|
|
161
|
-
read_in_full: number;
|
|
162
|
-
}>;
|
|
228
|
+
export declare const mapNotes: (token: string, pages: NoteSummary[], { deadlineMs, now }?: MapNotesOptions) => Promise<MapNotesResult>;
|
|
163
229
|
/** A page attachment as `read_note` reports it: named, never read. */
|
|
164
230
|
export type PageAttachment = Omit<PageResource, 'position'>;
|
|
165
231
|
export type NoteContent = {
|
|
@@ -207,3 +273,4 @@ export type NoteContent = {
|
|
|
207
273
|
export declare const readNote: (token: string, noteId: unknown, fromPart?: unknown, { withEditIds }?: {
|
|
208
274
|
withEditIds?: boolean;
|
|
209
275
|
}) => Promise<NoteContent>;
|
|
276
|
+
export {};
|
package/dist/notes.js
CHANGED
|
@@ -6,11 +6,34 @@
|
|
|
6
6
|
* enumeration below, which exists for a reason that is not obvious from the
|
|
7
7
|
* code alone.
|
|
8
8
|
*/
|
|
9
|
+
import { createHash } from 'node:crypto';
|
|
9
10
|
import { MAX_TEXT_CHARS } from './attachments.js';
|
|
10
11
|
import { GraphError } from './client.js';
|
|
11
|
-
import { graphGet } from './api.js';
|
|
12
|
+
import { FANOUT_LIMIT, graphGet, mapWithConcurrency } from './api.js';
|
|
12
13
|
import { pageResources } from './page-attachments.js';
|
|
13
14
|
import { editablePartsFrom } from './onenote-patch.js';
|
|
15
|
+
/**
|
|
16
|
+
* A short value proving a caller has actually seen this account's notebook list.
|
|
17
|
+
*
|
|
18
|
+
* The gate it serves used to be a module-level boolean — "has anything listed
|
|
19
|
+
* the notebooks yet" — which is a fair approximation of one CLI session and
|
|
20
|
+
* nonsense on hosted, where every request may land on a different instance and
|
|
21
|
+
* the flag is shared by every user on the one it lands on. It refused real
|
|
22
|
+
* selections on a cold instance and admitted unseen ones on a warm one.
|
|
23
|
+
*
|
|
24
|
+
* Derived from the notebook names rather than stored, so it needs no session
|
|
25
|
+
* and no shared storage: the same account yields the same key on any instance,
|
|
26
|
+
* a client that never saw the list cannot produce it, and a key from an account
|
|
27
|
+
* with different notebooks does not match. It is not a secret and does not need
|
|
28
|
+
* to be — it proves the list was seen, nothing more.
|
|
29
|
+
*
|
|
30
|
+
* Order and case are normalised so the key does not change when Graph returns
|
|
31
|
+
* the same notebooks in a different order.
|
|
32
|
+
*/
|
|
33
|
+
export const notebookKeyFor = (names) => createHash('sha256')
|
|
34
|
+
.update([...names].map((n) => n.trim().toLowerCase()).sort().join('\n'))
|
|
35
|
+
.digest('hex')
|
|
36
|
+
.slice(0, 8);
|
|
14
37
|
/** Graph ids are opaque, but they are concatenated into URLs, so they are checked like any other value. */
|
|
15
38
|
const ONENOTE_ID = /^[A-Za-z0-9!._~-]{1,300}$/;
|
|
16
39
|
/**
|
|
@@ -89,6 +112,40 @@ export const htmlToText = (html) => html
|
|
|
89
112
|
.join('\n')
|
|
90
113
|
.replace(/\n{3,}/g, '\n\n')
|
|
91
114
|
.trim();
|
|
115
|
+
/**
|
|
116
|
+
* Which notebooks this account has, and nothing else.
|
|
117
|
+
*
|
|
118
|
+
* One Graph request. `listNotes` answers the same question as a side effect of
|
|
119
|
+
* fetching every page of every section — a hundred requests on an organised
|
|
120
|
+
* account — and the notebook question is asked far more often than the page
|
|
121
|
+
* one: every call that has to ask "which notebook?" paid for the pages of all
|
|
122
|
+
* of them to print a list of names.
|
|
123
|
+
*
|
|
124
|
+
* That is what was throttling the account. Graph refused with 20166, "the app
|
|
125
|
+
* has issued too many requests on behalf of this user", against a listing whose
|
|
126
|
+
* answer was already contained in the single request that starts it, since
|
|
127
|
+
* `parentNotebook` expands inline on the sections call.
|
|
128
|
+
*
|
|
129
|
+
* Counted in sections rather than pages, because a page count is exactly the
|
|
130
|
+
* thing that cannot be known without the hundred requests. A number that costs
|
|
131
|
+
* a hundred requests to be slightly more familiar is not worth it, and the
|
|
132
|
+
* count is only there to help the user recognise which notebook is which.
|
|
133
|
+
*/
|
|
134
|
+
export const listNotebooks = async (token) => {
|
|
135
|
+
const res = await graphGet('/me/onenote/sections?$select=id,displayName' +
|
|
136
|
+
'&$expand=parentNotebook($select=displayName)&$top=100', token);
|
|
137
|
+
const sections = (await res.json()).value ?? [];
|
|
138
|
+
const counts = new Map();
|
|
139
|
+
for (const section of sections) {
|
|
140
|
+
if (typeof section.id !== 'string' || !ONENOTE_ID.test(section.id))
|
|
141
|
+
continue;
|
|
142
|
+
const name = section.parentNotebook?.displayName ?? '(unnamed notebook)';
|
|
143
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
notebooks: [...counts].map(([name, sectionCount]) => ({ name, sections: sectionCount })),
|
|
147
|
+
};
|
|
148
|
+
};
|
|
92
149
|
/**
|
|
93
150
|
* `/me/onenote/pages` looks like the obvious call and works right up until the
|
|
94
151
|
* account has too many sections, at which point Graph fails the whole request
|
|
@@ -99,46 +156,68 @@ export const htmlToText = (html) => html
|
|
|
99
156
|
* Do not "simplify" this back to the single call.
|
|
100
157
|
*/
|
|
101
158
|
export const listNotes = async (token) => {
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
159
|
+
const startedAt = Date.now();
|
|
160
|
+
let sectionCount = 0;
|
|
161
|
+
let failure = null;
|
|
162
|
+
try {
|
|
163
|
+
const sectionsRes = await graphGet(
|
|
164
|
+
// lastModifiedDateTime is what the detector reads, and it is the field
|
|
165
|
+
// that actually tracks change — unlike the one on a page. Selected here
|
|
166
|
+
// rather than fetched separately: the request is made either way.
|
|
167
|
+
'/me/onenote/sections?$select=id,displayName,lastModifiedDateTime' +
|
|
168
|
+
'&$expand=parentNotebook($select=displayName)&$top=100', token);
|
|
169
|
+
const sections = (await sectionsRes.json()).value ?? [];
|
|
170
|
+
const usable = sections.filter((s) => typeof s.id === 'string' && ONENOTE_ID.test(s.id));
|
|
171
|
+
sectionCount = usable.length;
|
|
172
|
+
const perSection = await mapWithConcurrency(usable, FANOUT_LIMIT, async (section) => {
|
|
173
|
+
const res = await graphGet(`/me/onenote/sections/${section.id}/pages` +
|
|
174
|
+
// createdDateTime is selected only so the two can be compared. It
|
|
175
|
+
// costs nothing — the request is made either way.
|
|
176
|
+
'?$select=id,title,createdDateTime,lastModifiedDateTime&$top=100', token);
|
|
177
|
+
const pages = (await res.json()).value ?? [];
|
|
178
|
+
return pages.map((p) => ({
|
|
179
|
+
id: p.id,
|
|
180
|
+
title: p.title ?? '(untitled)',
|
|
181
|
+
section: section.displayName ?? null,
|
|
182
|
+
notebook: section.parentNotebook?.displayName ?? null,
|
|
183
|
+
last_modified: p.lastModifiedDateTime ?? null,
|
|
184
|
+
}));
|
|
185
|
+
});
|
|
186
|
+
const sectionSummaries = usable.map((section, i) => ({
|
|
187
|
+
id: section.id,
|
|
188
|
+
name: section.displayName ?? '(unnamed section)',
|
|
120
189
|
notebook: section.parentNotebook?.displayName ?? null,
|
|
121
|
-
last_modified:
|
|
190
|
+
last_modified: section.lastModifiedDateTime ?? null,
|
|
191
|
+
pages: (perSection[i] ?? []).length,
|
|
122
192
|
}));
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
193
|
+
const creationDates = pageDatesAreCreationDates(sectionSummaries, perSection);
|
|
194
|
+
// Newest first, matching the order the single-call version happened to
|
|
195
|
+
// return; pages with no timestamp sort last rather than jumping to the top.
|
|
196
|
+
const notes = perSection
|
|
197
|
+
.flat()
|
|
198
|
+
.map((n) => ({ ...n, date_is_creation: creationDates }))
|
|
199
|
+
.sort((a, b) => (b.last_modified ?? '').localeCompare(a.last_modified ?? ''));
|
|
200
|
+
// Sections newest first for the same reason, and because when page dates are
|
|
201
|
+
// useless this is the ordering the caller actually reads.
|
|
202
|
+
const sectionList = sectionSummaries.sort((a, b) => (b.last_modified ?? '').localeCompare(a.last_modified ?? ''));
|
|
203
|
+
return { notes, sections: sectionList, page_dates_are_creation_dates: creationDates };
|
|
204
|
+
}
|
|
205
|
+
catch (err) {
|
|
206
|
+
failure = err instanceof Error ? err.message : String(err);
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
// Logged in a `finally`, and that is the whole point of it.
|
|
211
|
+
//
|
|
212
|
+
// The survey's own line sat after its await, so a run that threw logged
|
|
213
|
+
// nothing — and a throttled listing throws HERE, before the survey is ever
|
|
214
|
+
// reached, which is why a failed map produced nineteen 429s and no
|
|
215
|
+
// evidence. A measurement that exists only when nothing went wrong
|
|
216
|
+
// measures the wrong runs.
|
|
217
|
+
console.warn(`[artist-mcp] list_notes sections=${sectionCount} ` +
|
|
218
|
+
`elapsed=${Date.now() - startedAt}ms` +
|
|
219
|
+
(failure === null ? '' : ` failed=${failure}`));
|
|
220
|
+
}
|
|
142
221
|
};
|
|
143
222
|
/**
|
|
144
223
|
* The sections that changed within a window.
|
|
@@ -251,6 +330,25 @@ const previewOf = async (token, id) => {
|
|
|
251
330
|
const { previewText } = (await res.json());
|
|
252
331
|
return (previewText ?? '').trim();
|
|
253
332
|
};
|
|
333
|
+
/**
|
|
334
|
+
* How long a survey may spend before it returns what it has.
|
|
335
|
+
*
|
|
336
|
+
* The hosted route dies at sixty seconds (`maxDuration` in the MCP route), and
|
|
337
|
+
* dying there is the worst available outcome: the caller waits the full minute
|
|
338
|
+
* and receives nothing, not even the pages that were already sketched. Three
|
|
339
|
+
* of those in a row is what a chat client's retries look like from the logs.
|
|
340
|
+
*
|
|
341
|
+
* Forty-five leaves room for the answer to be assembled and sent after the
|
|
342
|
+
* last sketch lands. The number is a floor on usefulness rather than a tuning
|
|
343
|
+
* knob: whatever the notebook looks like, the survey now returns.
|
|
344
|
+
*/
|
|
345
|
+
export const MAP_DEADLINE_MS = 45_000;
|
|
346
|
+
/**
|
|
347
|
+
* A page the deadline was reached before. Not an error and not an empty page —
|
|
348
|
+
* distinguishing it from both is the whole point, because a page that was never
|
|
349
|
+
* looked at must never read as a page with nothing on it.
|
|
350
|
+
*/
|
|
351
|
+
const NOT_REACHED = Symbol('not reached');
|
|
254
352
|
/**
|
|
255
353
|
* Sketch every page in a chosen notebook, cheaply, so a notebook can be triaged
|
|
256
354
|
* without reading all of it.
|
|
@@ -267,61 +365,102 @@ const previewOf = async (token, id) => {
|
|
|
267
365
|
*
|
|
268
366
|
* Scoped to one notebook, as intake requires. Nothing maps across scopes.
|
|
269
367
|
*/
|
|
270
|
-
export const mapNotes = async (token, pages) => {
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
368
|
+
export const mapNotes = async (token, pages, { deadlineMs = MAP_DEADLINE_MS, now = Date.now } = {}) => {
|
|
369
|
+
const mapStartedAt = now();
|
|
370
|
+
const stopAt = mapStartedAt + deadlineMs;
|
|
371
|
+
// Held outside the try so the `finally` can report them whatever happens.
|
|
372
|
+
let sketched = 0;
|
|
373
|
+
let readInFull = 0;
|
|
374
|
+
let notReached = 0;
|
|
375
|
+
let mapFailure = null;
|
|
376
|
+
try {
|
|
377
|
+
const outcomes = await mapWithConcurrency(pages, FANOUT_LIMIT, async (page) => {
|
|
378
|
+
// Checked before the work, not during it: a page that has started is
|
|
379
|
+
// allowed to finish, because abandoning a request mid-flight costs the
|
|
380
|
+
// same quota as completing it and returns nothing for the spend.
|
|
381
|
+
if (now() >= stopAt) {
|
|
382
|
+
notReached += 1;
|
|
383
|
+
return NOT_REACHED;
|
|
384
|
+
}
|
|
385
|
+
const base = { ...page, chars_total: null, error: null };
|
|
386
|
+
let preview = '';
|
|
387
|
+
let reason = null;
|
|
388
|
+
try {
|
|
389
|
+
preview = await previewOf(token, page.id);
|
|
390
|
+
// A preview this thin cannot separate a working unit from a stray
|
|
391
|
+
// note, which is the one thing the map exists to do.
|
|
392
|
+
if (preview === '')
|
|
393
|
+
reason = 'the page has no preview text';
|
|
394
|
+
else if (preview.length < PREVIEW_FLOOR)
|
|
395
|
+
reason = `the preview was only ${preview.length} characters`;
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
reason = `the preview call failed (${err instanceof Error ? err.message : String(err)})`;
|
|
399
|
+
}
|
|
400
|
+
if (reason === null) {
|
|
401
|
+
return {
|
|
402
|
+
...base,
|
|
403
|
+
sketch: preview,
|
|
404
|
+
source: 'preview',
|
|
405
|
+
fell_back: null,
|
|
406
|
+
// Graph gives no page length and does not say it truncated, so a
|
|
407
|
+
// preview at full length is the only sign there is more. Inferred,
|
|
408
|
+
// and reported as an inference rather than as a fact.
|
|
409
|
+
more: preview.length >= PREVIEW_LIKELY_CAP,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
// Case by case, and only for the pages that need it.
|
|
413
|
+
try {
|
|
414
|
+
const { text, chars_total } = await readNote(token, page.id);
|
|
415
|
+
return {
|
|
416
|
+
...base,
|
|
417
|
+
sketch: text.slice(0, DERIVED_SKETCH_CHARS),
|
|
418
|
+
source: 'page',
|
|
419
|
+
fell_back: reason,
|
|
420
|
+
more: chars_total > DERIVED_SKETCH_CHARS,
|
|
421
|
+
chars_total,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
catch (err) {
|
|
425
|
+
// Both routes failed. Reported as a gap, never dropped from the map:
|
|
426
|
+
// a page missing from a survey reads as a page that is not there.
|
|
427
|
+
return {
|
|
428
|
+
...base,
|
|
429
|
+
sketch: null,
|
|
430
|
+
source: 'none',
|
|
431
|
+
fell_back: reason,
|
|
432
|
+
more: false,
|
|
433
|
+
error: err instanceof Error ? err.message : String(err),
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
const sketches = outcomes.filter((o) => o !== NOT_REACHED);
|
|
438
|
+
sketched = sketches.length;
|
|
439
|
+
readInFull = sketches.filter((s) => s.source === 'page').length;
|
|
440
|
+
return {
|
|
441
|
+
sketches,
|
|
442
|
+
read_in_full: readInFull,
|
|
443
|
+
not_reached: notReached,
|
|
444
|
+
elapsed_ms: now() - mapStartedAt,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
mapFailure = err instanceof Error ? err.message : String(err);
|
|
449
|
+
throw err;
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
// The line that says where the time went — on every run, the failed ones
|
|
453
|
+
// included, because those are the runs actually worth measuring.
|
|
454
|
+
//
|
|
455
|
+
// Fallbacks are the suspect: a preview costs one request, a fallback costs
|
|
456
|
+
// two plus the page's whole HTML, and arithmetic says forty previews at
|
|
457
|
+
// four concurrent should take about five seconds rather than sixty. This
|
|
458
|
+
// settles it with evidence instead.
|
|
459
|
+
console.warn(`[artist-mcp] map_notes pages=${pages.length} sketched=${sketched} ` +
|
|
460
|
+
`read_in_full=${readInFull} not_reached=${notReached} ` +
|
|
461
|
+
`elapsed=${now() - mapStartedAt}ms` +
|
|
462
|
+
(mapFailure === null ? '' : ` failed=${mapFailure}`));
|
|
463
|
+
}
|
|
325
464
|
};
|
|
326
465
|
/**
|
|
327
466
|
* Read one page, in parts only when it is too long for a single answer.
|
package/dist/onenote-patch.js
CHANGED
|
@@ -236,7 +236,27 @@ const ATTRS_BY_TAG = {
|
|
|
236
236
|
td: new Set(['colspan', 'rowspan', 'align', 'valign', 'width']),
|
|
237
237
|
th: new Set(['colspan', 'rowspan', 'align', 'valign', 'width']),
|
|
238
238
|
a: new Set(['href']),
|
|
239
|
+
// A checkbox is a `data-tag` on the paragraph, so without this a task can be
|
|
240
|
+
// read and never written: `htmlToText` has rendered these as `[ ]` and `[x]`
|
|
241
|
+
// since before anything could write, and a patch carrying one was refused.
|
|
242
|
+
// Per-tag rather than global, because a tag on a table cell or a heading is
|
|
243
|
+
// not a task and OneNote renders it somewhere nobody asked for a checkbox.
|
|
244
|
+
p: new Set(['data-tag']),
|
|
245
|
+
li: new Set(['data-tag']),
|
|
239
246
|
};
|
|
247
|
+
/**
|
|
248
|
+
* The only `data-tag` values this tool will write.
|
|
249
|
+
*
|
|
250
|
+
* OneNote defines dozens — `important`, `question`, `remember-for-later` and
|
|
251
|
+
* the rest — and every one of them is a claim about the line it marks. These
|
|
252
|
+
* two are the only ones with an agreed meaning here: a task, and a task that is
|
|
253
|
+
* done. Anything else is a model deciding that something is important, which is
|
|
254
|
+
* exactly the kind of judgement that must not arrive as page markup.
|
|
255
|
+
*
|
|
256
|
+
* Narrow on purpose, and cheap to widen if a use turns up that is really asked
|
|
257
|
+
* for rather than merely available.
|
|
258
|
+
*/
|
|
259
|
+
const TASK_TAGS = new Set(['to-do', 'to-do:completed']);
|
|
240
260
|
/**
|
|
241
261
|
* What may appear in a `style`, which OneNote's own tables lean on heavily for
|
|
242
262
|
* borders and widths — so forbidding it outright would mean every replaced
|
|
@@ -274,6 +294,13 @@ const checkAttributes = (tag, raw) => {
|
|
|
274
294
|
'OneNote. Nothing was written. Keep the markup to tables, rows, cells, ' +
|
|
275
295
|
'paragraphs, lists and simple emphasis.');
|
|
276
296
|
}
|
|
297
|
+
if (name === 'data-tag' && !TASK_TAGS.has(value)) {
|
|
298
|
+
throw failure(`<${tag}> carries data-tag="${value}", which this tool will not send to ` +
|
|
299
|
+
'OneNote. Nothing was written. Only "to-do" and "to-do:completed" are ' +
|
|
300
|
+
'written: an open task and a done one. Every other OneNote tag states ' +
|
|
301
|
+
'something about the line — that it is important, or a question — and ' +
|
|
302
|
+
'that is the musician\'s to say, not this tool\'s.');
|
|
303
|
+
}
|
|
277
304
|
if (name === 'style' && !SAFE_STYLE.test(value)) {
|
|
278
305
|
throw failure(`The style on <${tag}> contains something this tool will not send: ` +
|
|
279
306
|
`"${value}". Nothing was written. Plain declarations such as ` +
|
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 } from "./notes.js";
|
|
9
|
+
import { narrowNotes, narrowSections, notebookKeyFor } 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. */
|
|
@@ -26,20 +26,6 @@ const describeSize = (bytes) => {
|
|
|
26
26
|
return `${Math.round(bytes / 1024)} KB`;
|
|
27
27
|
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
28
28
|
};
|
|
29
|
-
/**
|
|
30
|
-
* Whether this server has ever handed back the list of notebooks.
|
|
31
|
-
*
|
|
32
|
-
* The one thing the server can actually know about where a notebook name came
|
|
33
|
-
* from. It cannot see who typed it — but a name arriving before the list was
|
|
34
|
-
* ever served cannot have come from the tool, so it came from somewhere outside
|
|
35
|
-
* the conversation. That is the case worth catching: a session inferred a
|
|
36
|
-
* notebook from saved context and answered about the wrong one, correctly and
|
|
37
|
-
* without saying which.
|
|
38
|
-
*
|
|
39
|
-
* Only ever set to true, and only by serving the list. It says nothing after a
|
|
40
|
-
* session has seen the notebooks once, which is the honest limit of it.
|
|
41
|
-
*/
|
|
42
|
-
let notebooksHaveBeenListed = false;
|
|
43
29
|
/**
|
|
44
30
|
* Settle which notebook is being worked in, before anything reads a page.
|
|
45
31
|
*
|
|
@@ -48,46 +34,63 @@ let notebooksHaveBeenListed = false;
|
|
|
48
34
|
* intake depends on, and two copies of it would eventually disagree. The caller
|
|
49
35
|
* names itself so the instruction says which tool to call again.
|
|
50
36
|
*/
|
|
51
|
-
const selectNotebook = async (call, notebook, tool) => {
|
|
52
|
-
|
|
53
|
-
|
|
37
|
+
const selectNotebook = async (call, notebook, notebookKey, tool) => {
|
|
38
|
+
// The notebook question first, and on its own, because it is the cheap one.
|
|
39
|
+
//
|
|
40
|
+
// `list_notes` answers it as a side effect of fetching every page of every
|
|
41
|
+
// section — a hundred Graph requests on an organised account — and most calls
|
|
42
|
+
// that reach here only need the names: to ask which notebook, or to check
|
|
43
|
+
// that a supplied one exists. Paying for every page of every notebook to
|
|
44
|
+
// print a list of names is what had Graph refusing with 20166.
|
|
45
|
+
const { notebooks } = await call("list_notebooks");
|
|
46
|
+
if (notebooks.length === 0)
|
|
54
47
|
return { message: "No notes found." };
|
|
55
|
-
const names =
|
|
48
|
+
const names = notebooks.map((n) => n.name);
|
|
56
49
|
// Handing back every page across every notebook invites work on the wrong
|
|
57
50
|
// one. With a choice to be made and nothing chosen, the pages are withheld
|
|
58
51
|
// until the user has actually made it.
|
|
59
52
|
// A name that arrives before this session has ever seen the list did not come
|
|
60
53
|
// from the tool, so it is either the user's or a guess — and the two are
|
|
61
54
|
// indistinguishable from here. Ask, the same way an omitted name asks.
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
55
|
+
// Proven against the account's own notebooks rather than against a flag this
|
|
56
|
+
// process happens to hold. Required only where the choice is real: one
|
|
57
|
+
// notebook is not a choice, and demanding proof of a list with a single entry
|
|
58
|
+
// would be ceremony.
|
|
59
|
+
const expected = notebookKeyFor(names);
|
|
60
|
+
const proven = notebookKey !== undefined && notebookKey.trim().toLowerCase() === expected;
|
|
61
|
+
const unproven = notebook !== undefined && !proven;
|
|
62
|
+
if ((!notebook || unproven) && names.length > 1) {
|
|
63
|
+
// Sections rather than pages: a page count is precisely the thing that
|
|
64
|
+
// cannot be known without the hundred requests this path exists to avoid,
|
|
65
|
+
// and the number is only here to help the user tell one notebook from
|
|
66
|
+
// another.
|
|
67
|
+
const counts = notebooks.map(({ name, sections: n }) => `- ${name} — ${n} section${n === 1 ? "" : "s"}`);
|
|
69
68
|
return {
|
|
70
69
|
message: `This account has ${names.length} notebooks:\n${counts.join("\n")}\n\n` +
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"
|
|
70
|
+
`notebook_key: ${expected}\n\n` +
|
|
71
|
+
(unproven
|
|
72
|
+
? `You asked for "${notebook}" without the notebook_key from this ` +
|
|
73
|
+
"list, so nothing in this conversation had seen the notebooks yet. " +
|
|
74
|
+
"Ask the user which one they mean — including whether it is that " +
|
|
75
|
+
`one — and call ${tool} again with that name AND the notebook_key ` +
|
|
76
|
+
"above. A notebook you know of from elsewhere is a guess, and a " +
|
|
77
|
+
"guess here produces an answer that is correct about the wrong pages."
|
|
78
78
|
: `Ask the user which notebook to work in, then call ${tool} again ` +
|
|
79
|
-
"with that name. Do not guess, and do
|
|
80
|
-
"unless the user asks for it."),
|
|
79
|
+
"with that name and the notebook_key above. Do not guess, and do " +
|
|
80
|
+
"not work across notebooks unless the user asks for it."),
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
notebooksHaveBeenListed = true;
|
|
84
83
|
const wanted = notebook?.trim().toLowerCase();
|
|
84
|
+
// Checked before the expensive call, not after it: a misspelled notebook
|
|
85
|
+
// should cost one request to refuse, not a hundred.
|
|
86
|
+
if (wanted !== undefined && !names.some((name) => name.trim().toLowerCase() === wanted)) {
|
|
87
|
+
return { message: `No notebook named "${notebook}". Available: ${names.join(", ")}.` };
|
|
88
|
+
}
|
|
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");
|
|
85
91
|
const pages = wanted
|
|
86
92
|
? notes.filter((n) => (n.notebook ?? "").trim().toLowerCase() === wanted)
|
|
87
93
|
: notes;
|
|
88
|
-
if (wanted && pages.length === 0) {
|
|
89
|
-
return { message: `No notebook named "${notebook}". Available: ${names.join(", ")}.` };
|
|
90
|
-
}
|
|
91
94
|
// A name that was supplied walks straight past the question above, so a
|
|
92
95
|
// guessed one is indistinguishable from a chosen one. Found in use: asked
|
|
93
96
|
// about "this notebook" in a fresh chat, a session inferred one from saved
|
|
@@ -173,7 +176,7 @@ const renderChangedSections = (chosen, notebook, since) => {
|
|
|
173
176
|
* the only way OneNote allows a cell to change.
|
|
174
177
|
*/
|
|
175
178
|
const INDEX_ENTRIES = 12;
|
|
176
|
-
const serverVersion = '2.
|
|
179
|
+
const serverVersion = '2.3.0'; // x-release-please-version
|
|
177
180
|
const errorResult = (err) => {
|
|
178
181
|
const message = err instanceof GraphError ? err.message : `Unexpected error: ${err}`;
|
|
179
182
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -715,6 +718,14 @@ const createServer = async (call, grants = []) => {
|
|
|
715
718
|
"notebook you happen to know the user has: a plausible guess here " +
|
|
716
719
|
"is indistinguishable from their choice and produces a confident " +
|
|
717
720
|
"answer about the wrong notebook."),
|
|
721
|
+
notebook_key: z
|
|
722
|
+
.string()
|
|
723
|
+
.optional()
|
|
724
|
+
.describe("The notebook_key printed with the notebook list, passed back exactly " +
|
|
725
|
+
"as it was given. Required alongside `notebook` on an account with " +
|
|
726
|
+
"more than one notebook: it is how this tool knows the name came " +
|
|
727
|
+
"from the list rather than from somewhere outside the conversation. " +
|
|
728
|
+
"Never invent one, and never reuse one from an earlier session."),
|
|
718
729
|
since: z
|
|
719
730
|
.string()
|
|
720
731
|
.optional()
|
|
@@ -734,9 +745,9 @@ const createServer = async (call, grants = []) => {
|
|
|
734
745
|
.describe("Cap the number of pages returned, newest first. The reply says how " +
|
|
735
746
|
"many matched, so a capped list is never mistaken for the whole " +
|
|
736
747
|
"notebook."),
|
|
737
|
-
}, async ({ notebook, since, limit }) => {
|
|
748
|
+
}, async ({ notebook, notebook_key, since, limit }) => {
|
|
738
749
|
try {
|
|
739
|
-
const chosen = await selectNotebook(call, notebook, "list_notes");
|
|
750
|
+
const chosen = await selectNotebook(call, notebook, notebook_key, "list_notes");
|
|
740
751
|
if ("message" in chosen) {
|
|
741
752
|
return { content: [{ type: "text", text: chosen.message }] };
|
|
742
753
|
}
|
|
@@ -832,6 +843,14 @@ const createServer = async (call, grants = []) => {
|
|
|
832
843
|
"this conversation, or named by the user in it. Omit when they have " +
|
|
833
844
|
"not chosen one. Never fill it in from saved context or an earlier " +
|
|
834
845
|
"session."),
|
|
846
|
+
notebook_key: z
|
|
847
|
+
.string()
|
|
848
|
+
.optional()
|
|
849
|
+
.describe("The notebook_key printed with the notebook list, passed back exactly " +
|
|
850
|
+
"as it was given. Required alongside `notebook` on an account with " +
|
|
851
|
+
"more than one notebook: it is how this tool knows the name came " +
|
|
852
|
+
"from the list rather than from somewhere outside the conversation. " +
|
|
853
|
+
"Never invent one, and never reuse one from an earlier session."),
|
|
835
854
|
since: z
|
|
836
855
|
.string()
|
|
837
856
|
.optional()
|
|
@@ -844,9 +863,9 @@ const createServer = async (call, grants = []) => {
|
|
|
844
863
|
.min(1)
|
|
845
864
|
.optional()
|
|
846
865
|
.describe(`Cap how many pages are sketched, newest first. Defaults to ${DEFAULT_MAP_PAGES}.`),
|
|
847
|
-
}, async ({ notebook, since, limit }) => {
|
|
866
|
+
}, async ({ notebook, notebook_key, since, limit }) => {
|
|
848
867
|
try {
|
|
849
|
-
const chosen = await selectNotebook(call, notebook, "map_notes");
|
|
868
|
+
const chosen = await selectNotebook(call, notebook, notebook_key, "map_notes");
|
|
850
869
|
if ("message" in chosen) {
|
|
851
870
|
return { content: [{ type: "text", text: chosen.message }] };
|
|
852
871
|
}
|
|
@@ -898,7 +917,7 @@ const createServer = async (call, grants = []) => {
|
|
|
898
917
|
],
|
|
899
918
|
};
|
|
900
919
|
}
|
|
901
|
-
const { sketches, read_in_full } = await call("map_notes", { pages });
|
|
920
|
+
const { sketches, read_in_full, not_reached } = await call("map_notes", { pages });
|
|
902
921
|
const blocks = sketches.map((s) => {
|
|
903
922
|
const location = [s.notebook, s.section].filter(Boolean).join(" / ");
|
|
904
923
|
const head = `## ${s.title}${location ? ` (${location})` : ""} — dated ${s.last_modified ?? "unknown"}` +
|
|
@@ -930,6 +949,16 @@ const createServer = async (call, grants = []) => {
|
|
|
930
949
|
"usable preview and were read in full instead, so those sketches " +
|
|
931
950
|
"cover more of the page than the rest.");
|
|
932
951
|
}
|
|
952
|
+
if (not_reached > 0) {
|
|
953
|
+
// Said before the other truncations, because it is the one the
|
|
954
|
+
// caller did not ask for: `limit` is their own cap and this is the
|
|
955
|
+
// clock running out. A partial survey that does not say so is a
|
|
956
|
+
// survey the caller will read as complete.
|
|
957
|
+
caveats.push(`Stopped after ${sketches.length} of ${sketches.length + not_reached} pages: the ` +
|
|
958
|
+
"survey ran out of time before the rest were reached. Those pages are " +
|
|
959
|
+
"UNSURVEYED, not empty — call map_notes again with a smaller `limit` to " +
|
|
960
|
+
"cover them, or read_note the ones you already know you need.");
|
|
961
|
+
}
|
|
933
962
|
if (pages.length < matched) {
|
|
934
963
|
caveats.push(`Sketched the ${pages.length} newest by date of ${matched} pages. ` +
|
|
935
964
|
"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
|
+
"version": "2.3.0",
|
|
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",
|