@manudota/artist-mcp 2.0.1-staging.94 → 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/agent-pack/.artist/policies/PATCH.md +65 -11
- package/agent-pack/registry.json +2 -2
- package/dist/api.d.ts +37 -0
- package/dist/api.js +83 -0
- package/dist/audit.d.ts +16 -0
- package/dist/connect.js +20 -5
- package/dist/dispatch.d.ts +8 -0
- package/dist/dispatch.js +15 -0
- package/dist/grants.d.ts +1 -0
- package/dist/grants.js +13 -0
- package/dist/oauth.js +12 -0
- package/dist/onenote-patch.d.ts +178 -0
- package/dist/onenote-patch.js +403 -0
- package/dist/server.js +117 -2
- package/package.json +1 -1
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
# Patch
|
|
2
2
|
|
|
3
3
|
Once the musician has decided something, hand them the smallest fragment that
|
|
4
|
-
records it, and say which part of the page it goes under.
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
records it, and say which part of the page it goes under.
|
|
5
|
+
|
|
6
|
+
Usually they paste it, and the page is unchanged until they do. On the pages
|
|
7
|
+
this tool made itself it can apply the fragment instead, having shown it and
|
|
8
|
+
been told yes. Which of the two applies is decided in "Whose page is it" below,
|
|
9
|
+
and it is the only thing about a patch that changed: everything else here was
|
|
10
|
+
never a consequence of being unable to write.
|
|
7
11
|
|
|
8
12
|
A recommendation they agreed with and a page that still does not say it is the
|
|
9
13
|
same finding waiting to be re-derived next week. The page is the state. Ending
|
|
@@ -22,6 +26,13 @@ Offer once, alongside the answer, and do not ask again if they leave it. Whether
|
|
|
22
26
|
their pages get updated is theirs to decide, and a second offer reads as
|
|
23
27
|
chasing.
|
|
24
28
|
|
|
29
|
+
Applying is not a lighter kind of offering. Show the change and wait for a yes
|
|
30
|
+
in words, every time, exactly as creating a page and every calendar write does —
|
|
31
|
+
an edit is smaller and that is not a reason to ask less. The confirmation the
|
|
32
|
+
tool requires is a check that the values are the ones displayed; it cannot tell
|
|
33
|
+
whether anybody read them. A patch they leave is left, and one they decline is
|
|
34
|
+
not applied later because the conversation moved on.
|
|
35
|
+
|
|
25
36
|
**One patch records a finding rather than a decision**: the cross-reference in
|
|
26
37
|
`policy:divergence`, offered from the moment two pages are suspected of being
|
|
27
38
|
one event and not from when it is confirmed. It is the exception because it
|
|
@@ -30,6 +41,28 @@ they later conclude. Everything above still applies to it. Do not withhold it
|
|
|
30
41
|
for want of a decision; a twin found today is otherwise unknown tomorrow, and
|
|
31
42
|
the finding dies with the session that made it.
|
|
32
43
|
|
|
44
|
+
## Whose page is it
|
|
45
|
+
|
|
46
|
+
An install granted `onenote-edit` can change **pages this tool created** and no
|
|
47
|
+
others. Microsoft refuses the rest, so that is not a rule to keep — but it is
|
|
48
|
+
also not a rule you can check, because **reading a page always works and only
|
|
49
|
+
writing is refused.** A preview of an edit to the musician's own page will
|
|
50
|
+
succeed, show a change and invite a yes, and the refusal lands afterwards.
|
|
51
|
+
|
|
52
|
+
So offer to apply only where you can see the page is this tool's own:
|
|
53
|
+
|
|
54
|
+
- **A page created in this conversation** — a consolidated page, a filled-out
|
|
55
|
+
template — may be offered as an applied change.
|
|
56
|
+
- **Everywhere else, hand it over to be pasted.** Their pages, pages from
|
|
57
|
+
earlier sessions, pages you merely believe this tool made: a fragment, as
|
|
58
|
+
always. This is narrower than the tool permits, deliberately. A needless paste
|
|
59
|
+
costs a moment; an offer that fails after they have said yes costs their trust
|
|
60
|
+
in every offer after it.
|
|
61
|
+
|
|
62
|
+
Never say which of the two it will be until you know. "I can update that for
|
|
63
|
+
you" about a page you have not established is this tool's own is a promise made
|
|
64
|
+
before the fact.
|
|
65
|
+
|
|
33
66
|
## The smallest fragment that records it
|
|
34
67
|
|
|
35
68
|
A one-line decision is one line. Never restate the page around the change, and
|
|
@@ -93,8 +126,15 @@ decision next to it.
|
|
|
93
126
|
|
|
94
127
|
## Form
|
|
95
128
|
|
|
96
|
-
|
|
97
|
-
|
|
129
|
+
An applied patch has no file. The preview is its form: it shows the text that
|
|
130
|
+
would be overwritten and the text replacing it, and that is what the musician
|
|
131
|
+
approves. Do not also produce a file for it — a fragment to paste alongside a
|
|
132
|
+
change already made is a second copy of the same decision, and the one they
|
|
133
|
+
paste will duplicate it.
|
|
134
|
+
|
|
135
|
+
A patch to be pasted is handed over the way a template is: as a file, following
|
|
136
|
+
Delivery and Shape in `policy:intake` rather than a second convention invented
|
|
137
|
+
here.
|
|
98
138
|
Self-contained HTML with visible table borders, never a code fence, never a
|
|
99
139
|
source format. One file per destination page, openable on its own — a decision
|
|
100
140
|
landing on two pages is two files, as a cross-reference is.
|
|
@@ -137,13 +177,27 @@ page says nothing about any of this until they paste it.
|
|
|
137
177
|
|
|
138
178
|
## Boundaries
|
|
139
179
|
|
|
140
|
-
- Never claim a page was changed
|
|
141
|
-
|
|
180
|
+
- Never claim a page was changed unless the tool reported changing it. A patch
|
|
181
|
+
offered, or previewed, or agreed to and not yet applied, is not a page that
|
|
182
|
+
says anything new.
|
|
183
|
+
- Never call a change undoable. OneNote keeps no version of a page and a recycle
|
|
184
|
+
bin does not catch what this writes; what was overwritten is kept in this
|
|
185
|
+
install's own write log, which is a real recovery and not one the musician can
|
|
186
|
+
perform in OneNote. Say that, rather than "I can put it back".
|
|
142
187
|
- Never track what was pasted or assume an earlier fragment landed. Nothing here
|
|
143
|
-
remembers, and a page that looks patched is a page you have re-read.
|
|
188
|
+
remembers, and a page that looks patched is a page you have re-read. This now
|
|
189
|
+
has a second reason: the ids that address a page's parts move every time it is
|
|
190
|
+
written to, so anything remembered about its structure is stale the moment it
|
|
191
|
+
changes.
|
|
144
192
|
- Never patch two pages into agreement with each other. Where two records of one
|
|
145
193
|
event conflict, `policy:divergence` governs, and a cross-reference is the only
|
|
146
194
|
fragment offered before the fields are settled.
|
|
147
|
-
- Never delete
|
|
148
|
-
|
|
149
|
-
|
|
195
|
+
- Never delete anything. Not a page, not a section of one, and there is no tool
|
|
196
|
+
that can — a deleted page is not in the notebook's recycle bin.
|
|
197
|
+
- Replace only text **this tool itself wrote**, and only where the musician has
|
|
198
|
+
now settled it differently — correcting a fee on a page it generated is the
|
|
199
|
+
case this exists for. A page it created is its own output right up until they
|
|
200
|
+
write on it, and their words are theirs wherever they put them.
|
|
201
|
+
- Where a decision contradicts a value already on the page and that value is not
|
|
202
|
+
this tool's own text, say both and let them choose what to do with the old
|
|
203
|
+
one. Unchanged, and it is the common case.
|
package/agent-pack/registry.json
CHANGED
|
@@ -45,9 +45,9 @@
|
|
|
45
45
|
"id": "policy:patch",
|
|
46
46
|
"kind": "policy",
|
|
47
47
|
"name": "Patch",
|
|
48
|
-
"description": "Once the musician has decided something, hand them the smallest fragment that records it, and say which part of the page it goes under.
|
|
48
|
+
"description": "Once the musician has decided something, hand them the smallest fragment that records it, and say which part of the page it goes under.",
|
|
49
49
|
"file": ".artist/policies/PATCH.md",
|
|
50
|
-
"sha256": "
|
|
50
|
+
"sha256": "d844496f2720befd02efa0ae940355e569acf2467e18906bad7e0769350653e2"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"id": "project-type:concert",
|
package/dist/api.d.ts
CHANGED
|
@@ -73,3 +73,40 @@ export declare const CALENDAR_LIST_NEED: ScopeNeed;
|
|
|
73
73
|
* See docs/decisions/0003-onenote-writes.md.
|
|
74
74
|
*/
|
|
75
75
|
export declare const onenoteCreatePage: (sectionId: string, xhtml: string, token: string) => Promise<Response>;
|
|
76
|
+
/**
|
|
77
|
+
* Read a page back with the ids a patch can aim at.
|
|
78
|
+
*
|
|
79
|
+
* `includeIDs=true` is not optional and not a nicety: without it Graph returns
|
|
80
|
+
* only the `data-id` values the page was created with, and a replace requires
|
|
81
|
+
* the generated `id`. Microsoft warns those "might change after a page update",
|
|
82
|
+
* so this is called immediately before the command that uses what it returns —
|
|
83
|
+
* never once per session, and never carried across a write.
|
|
84
|
+
*
|
|
85
|
+
* The ETag comes back too, because it is the only thing that can tell us the
|
|
86
|
+
* page moved between the read and the write. Verified honoured: a stale
|
|
87
|
+
* `If-Match` is answered 412 rather than ignored.
|
|
88
|
+
*/
|
|
89
|
+
export declare const onenotePageContent: (pageId: string, token: string) => Promise<{
|
|
90
|
+
html: string;
|
|
91
|
+
etag: string | null;
|
|
92
|
+
}>;
|
|
93
|
+
/**
|
|
94
|
+
* Apply patch commands to one page.
|
|
95
|
+
*
|
|
96
|
+
* Narrow in the same way `onenoteCreatePage` is, and here the narrowness is
|
|
97
|
+
* load-bearing rather than defensive: this is the only function in the package
|
|
98
|
+
* that can destroy something a musician might want back. A general
|
|
99
|
+
* `graphPatch(path)` would make that reachable from anywhere.
|
|
100
|
+
*
|
|
101
|
+
* Not retried. A 5xx does not say whether the patch landed, and a repeated
|
|
102
|
+
* replace against an id read before the first attempt would aim at whatever
|
|
103
|
+
* that id points at now. Where a caller wants to try again it must read the
|
|
104
|
+
* page again first, which is the same rule the ids themselves impose.
|
|
105
|
+
*
|
|
106
|
+
* See docs/decisions/0004-onenote-page-maintenance.md.
|
|
107
|
+
*/
|
|
108
|
+
export declare const onenotePatchPage: (pageId: string, commands: readonly {
|
|
109
|
+
target: string;
|
|
110
|
+
action: string;
|
|
111
|
+
content: string;
|
|
112
|
+
}[], token: string, etag: string | null) => Promise<void>;
|
package/dist/api.js
CHANGED
|
@@ -304,3 +304,86 @@ export const onenoteCreatePage = async (sectionId, xhtml, token) => {
|
|
|
304
304
|
}
|
|
305
305
|
throw new GraphError(`OneNote refused to create the page (${res.status}). ${detail}`, false);
|
|
306
306
|
};
|
|
307
|
+
/**
|
|
308
|
+
* Read a page back with the ids a patch can aim at.
|
|
309
|
+
*
|
|
310
|
+
* `includeIDs=true` is not optional and not a nicety: without it Graph returns
|
|
311
|
+
* only the `data-id` values the page was created with, and a replace requires
|
|
312
|
+
* the generated `id`. Microsoft warns those "might change after a page update",
|
|
313
|
+
* so this is called immediately before the command that uses what it returns —
|
|
314
|
+
* never once per session, and never carried across a write.
|
|
315
|
+
*
|
|
316
|
+
* The ETag comes back too, because it is the only thing that can tell us the
|
|
317
|
+
* page moved between the read and the write. Verified honoured: a stale
|
|
318
|
+
* `If-Match` is answered 412 rather than ignored.
|
|
319
|
+
*/
|
|
320
|
+
export const onenotePageContent = async (pageId, token) => {
|
|
321
|
+
// graphGet rather than a bare path: getWithRetry takes a whole URL, and the
|
|
322
|
+
// throttling and scope handling live in that wrapper.
|
|
323
|
+
const res = await graphGet(`/me/onenote/pages/${encodeURIComponent(pageId)}/content?includeIDs=true`, token);
|
|
324
|
+
return { html: await res.text(), etag: res.headers.get('etag') };
|
|
325
|
+
};
|
|
326
|
+
/**
|
|
327
|
+
* Apply patch commands to one page.
|
|
328
|
+
*
|
|
329
|
+
* Narrow in the same way `onenoteCreatePage` is, and here the narrowness is
|
|
330
|
+
* load-bearing rather than defensive: this is the only function in the package
|
|
331
|
+
* that can destroy something a musician might want back. A general
|
|
332
|
+
* `graphPatch(path)` would make that reachable from anywhere.
|
|
333
|
+
*
|
|
334
|
+
* Not retried. A 5xx does not say whether the patch landed, and a repeated
|
|
335
|
+
* replace against an id read before the first attempt would aim at whatever
|
|
336
|
+
* that id points at now. Where a caller wants to try again it must read the
|
|
337
|
+
* page again first, which is the same rule the ids themselves impose.
|
|
338
|
+
*
|
|
339
|
+
* See docs/decisions/0004-onenote-page-maintenance.md.
|
|
340
|
+
*/
|
|
341
|
+
export const onenotePatchPage = async (pageId, commands, token, etag) => {
|
|
342
|
+
const res = await fetch(`${GRAPH}/me/onenote/pages/${encodeURIComponent(pageId)}/content`, {
|
|
343
|
+
method: 'PATCH',
|
|
344
|
+
headers: {
|
|
345
|
+
authorization: `Bearer ${token}`,
|
|
346
|
+
'content-type': 'application/json',
|
|
347
|
+
...(etag === null ? {} : { 'if-match': etag }),
|
|
348
|
+
},
|
|
349
|
+
body: JSON.stringify(commands),
|
|
350
|
+
});
|
|
351
|
+
if (res.status === 204)
|
|
352
|
+
return;
|
|
353
|
+
const detail = (await res.text().catch(() => '')).slice(0, 300);
|
|
354
|
+
// The page changed under us. Reported as what it is rather than retried: the
|
|
355
|
+
// musician typing in OneNote while this ran is the likeliest cause, and
|
|
356
|
+
// writing over that is the exact harm the header exists to prevent.
|
|
357
|
+
if (res.status === 412) {
|
|
358
|
+
throw new GraphError('That page changed while this change was being prepared, so nothing was ' +
|
|
359
|
+
'written — the edit would have overwritten whatever was just done to it. ' +
|
|
360
|
+
'Ask again to work from the current version of the page.', false);
|
|
361
|
+
}
|
|
362
|
+
// 40003 is a page the musician wrote; 40006 is a page another application
|
|
363
|
+
// created. Both are Microsoft refusing on ownership, which is the boundary
|
|
364
|
+
// this capability rests on, so neither is reported as a fault.
|
|
365
|
+
if ((res.status === 401 || res.status === 403) && /40003|40006/.test(detail)) {
|
|
366
|
+
throw new GraphError('That page was not created by artist-mcp, so it cannot be changed. This ' +
|
|
367
|
+
'tool can only edit its own pages — a page you wrote is refused by ' +
|
|
368
|
+
'Microsoft, not by this tool, and that is deliberate.', false);
|
|
369
|
+
}
|
|
370
|
+
if ((res.status === 401 || res.status === 403) && /40004|scope/i.test(detail)) {
|
|
371
|
+
throw new ScopeError('This Microsoft connection cannot edit OneNote pages. Reconnect with ' +
|
|
372
|
+
'`artist-mcp connect microsoft` — a refresh token carries the scopes it ' +
|
|
373
|
+
'was granted with, so an existing connection cannot edit until renewed.', 'edit OneNote pages', false);
|
|
374
|
+
}
|
|
375
|
+
// 20134 means the target did not resolve, which for a data-id is the missing
|
|
376
|
+
// '#' and for a generated id means it moved since the read. 20138 means the
|
|
377
|
+
// element resolved and does not support that action. They read alike and are
|
|
378
|
+
// not alike, so they are told apart here rather than in a log nobody reads.
|
|
379
|
+
if (/20134/.test(detail)) {
|
|
380
|
+
throw new GraphError('That part of the page could not be found, which usually means the page ' +
|
|
381
|
+
'changed since it was read. Nothing was written. Ask again to work from ' +
|
|
382
|
+
'the current version.', false);
|
|
383
|
+
}
|
|
384
|
+
if (/20138/.test(detail)) {
|
|
385
|
+
throw new GraphError('That part of the page cannot be changed in that way — OneNote does not ' +
|
|
386
|
+
'allow it for that kind of element. Nothing was written.', false);
|
|
387
|
+
}
|
|
388
|
+
throw new GraphError(`OneNote refused to change the page (${res.status}). ${detail}`, false);
|
|
389
|
+
};
|
package/dist/audit.d.ts
CHANGED
|
@@ -42,6 +42,22 @@ export type WriteRecord = {
|
|
|
42
42
|
* docs/decisions/0003-onenote-writes.md.
|
|
43
43
|
*/
|
|
44
44
|
created_by_app_id?: string | null;
|
|
45
|
+
/**
|
|
46
|
+
* For a replaced OneNote element: exactly what was on the page beforehand.
|
|
47
|
+
*
|
|
48
|
+
* This is the undo, and there is no other one. OneNote keeps no version of a
|
|
49
|
+
* page and a Graph write leaves nothing in the recycle bin, so what stood
|
|
50
|
+
* where the change landed exists nowhere else the moment the PATCH returns
|
|
51
|
+
* 204. The audit line is not a note about the write here — it *is* the
|
|
52
|
+
* recovery, which is what the header of this file has always claimed the
|
|
53
|
+
* point of these records to be.
|
|
54
|
+
*
|
|
55
|
+
* Held as the element's HTML rather than its text, because restoring it means
|
|
56
|
+
* writing it back. It restores content and not bytes: OneNote rewrites the
|
|
57
|
+
* markup it stores, so a restored paragraph comes back carrying its words and
|
|
58
|
+
* not its original HTML. See docs/decisions/0004-onenote-page-maintenance.md.
|
|
59
|
+
*/
|
|
60
|
+
pre_image?: string | null;
|
|
45
61
|
};
|
|
46
62
|
/**
|
|
47
63
|
* Record a write that has already happened.
|
package/dist/connect.js
CHANGED
|
@@ -49,13 +49,28 @@ export const runConnect = async (input) => {
|
|
|
49
49
|
else {
|
|
50
50
|
// Named, because the consent screen the user just approved was wider than
|
|
51
51
|
// the one every other install sees, and they should be able to tell why.
|
|
52
|
+
// What Microsoft consent covered depends on which OneNote grants are held,
|
|
53
|
+
// and the difference is exactly the thing a user would want to know. This
|
|
54
|
+
// said unconditionally that no page could be changed "including the ones it
|
|
55
|
+
// creates — the permission it holds cannot express that", which was true of
|
|
56
|
+
// `Notes.Create` alone and becomes a false reassurance the moment
|
|
57
|
+
// `onenote-edit` is granted. A sentence that is wrong only for the widest
|
|
58
|
+
// install is the worst possible place to be wrong.
|
|
59
|
+
const canEdit = relevant.includes('onenote-edit');
|
|
60
|
+
const microsoft = canEdit
|
|
61
|
+
? 'creating OneNote pages and changing the ones it created. It cannot ' +
|
|
62
|
+
'touch a page you wrote or one another app created — Microsoft refuses ' +
|
|
63
|
+
'those — and it cannot delete anything. A change it makes to its own ' +
|
|
64
|
+
'page overwrites what was there, and OneNote keeps no version, so what ' +
|
|
65
|
+
'it replaced is kept only in this install\'s write log.'
|
|
66
|
+
: 'creating OneNote pages. No page can be changed or deleted by this ' +
|
|
67
|
+
'tool, including the ones it creates — the permission it holds cannot ' +
|
|
68
|
+
'express that.';
|
|
52
69
|
console.log(`This install was granted ${relevant.join(', ')}, so consent included ` +
|
|
53
|
-
`${provider === 'microsoft' ? 'creating OneNote pages' : 'creating calendar events'}. ` +
|
|
54
70
|
(provider === 'microsoft'
|
|
55
|
-
?
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'changed except by the reschedule you granted.'));
|
|
71
|
+
? microsoft
|
|
72
|
+
: 'creating calendar events. Nothing else is ever written back: not ' +
|
|
73
|
+
'mail, and no event is changed except by the reschedule you granted.'));
|
|
59
74
|
}
|
|
60
75
|
// Said at the one moment the user is certainly reading, which is the whole
|
|
61
76
|
// point: #94 describes a connection that dies weekly, an error that correctly
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -110,6 +110,14 @@ export declare const OPERATIONS: {
|
|
|
110
110
|
readonly provider: "microsoft";
|
|
111
111
|
readonly effect: "write";
|
|
112
112
|
};
|
|
113
|
+
readonly preview_onenote_edit: {
|
|
114
|
+
readonly provider: "microsoft";
|
|
115
|
+
readonly effect: "read";
|
|
116
|
+
};
|
|
117
|
+
readonly edit_onenote_page: {
|
|
118
|
+
readonly provider: "microsoft";
|
|
119
|
+
readonly effect: "write";
|
|
120
|
+
};
|
|
113
121
|
};
|
|
114
122
|
export type Operation = keyof typeof OPERATIONS;
|
|
115
123
|
/**
|
package/dist/dispatch.js
CHANGED
|
@@ -17,6 +17,7 @@ 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
19
|
import { listNotes, mapNotes, readNote } from './notes.js';
|
|
20
|
+
import { applyEdit, previewEdit } from './onenote-patch.js';
|
|
20
21
|
import { createPage, previewPage } from './onenote-write.js';
|
|
21
22
|
import { accessTokenFor } from './oauth.js';
|
|
22
23
|
import { loadTokens } from './tokens.js';
|
|
@@ -78,6 +79,16 @@ export const OPERATIONS = {
|
|
|
78
79
|
// `Notes.Create` cannot express an edit or a delete, so there is no sibling
|
|
79
80
|
// row here to refuse. See docs/decisions/0003-onenote-writes.md.
|
|
80
81
|
create_onenote_page: { provider: 'microsoft', effect: 'write' },
|
|
82
|
+
// Reads the page and shows the change against what is actually written there
|
|
83
|
+
// now. A read, and necessarily so: it is what has to happen before a write is
|
|
84
|
+
// allowed, and gating it would gate the safeguard rather than the danger.
|
|
85
|
+
preview_onenote_edit: { provider: 'microsoft', effect: 'read' },
|
|
86
|
+
// The first row here that can destroy something. Both actions ride one row
|
|
87
|
+
// and one grant because no scope separates them, so the distinction is made
|
|
88
|
+
// where it can be enforced — in the operation itself, where a replace cannot
|
|
89
|
+
// proceed unless what it would overwrite has been captured first.
|
|
90
|
+
// See docs/decisions/0004-onenote-page-maintenance.md.
|
|
91
|
+
edit_onenote_page: { provider: 'microsoft', effect: 'write' },
|
|
81
92
|
};
|
|
82
93
|
/** Kept as its own export: callers ask "which token" far more than "which effect". */
|
|
83
94
|
const PROVIDER_FOR = Object.fromEntries(Object.entries(OPERATIONS).map(([op, meta]) => [op, meta.provider]));
|
|
@@ -140,6 +151,10 @@ export const dispatchWith = (resolve, record = recordWrite) => async (op, params
|
|
|
140
151
|
return (await previewPage(token, params));
|
|
141
152
|
case 'create_onenote_page':
|
|
142
153
|
return (await createPage(token, params, record));
|
|
154
|
+
case 'preview_onenote_edit':
|
|
155
|
+
return (await previewEdit(token, params));
|
|
156
|
+
case 'edit_onenote_page':
|
|
157
|
+
return (await applyEdit(token, params, record));
|
|
143
158
|
case 'list_calendars':
|
|
144
159
|
// No parameters by design: the whole value is knowing the full set, and
|
|
145
160
|
// a filtered list of what exists is the reach problem again.
|
package/dist/grants.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare const WRITE_CAPABILITIES: {
|
|
|
31
31
|
readonly 'calendar-create': "add a single event to a Google Calendar, after showing it to you first";
|
|
32
32
|
readonly 'calendar-delete': string;
|
|
33
33
|
readonly 'onenote-create': string;
|
|
34
|
+
readonly 'onenote-edit': string;
|
|
34
35
|
};
|
|
35
36
|
export type WriteCapability = keyof typeof WRITE_CAPABILITIES;
|
|
36
37
|
/** Whether a bare token names a capability, for untangling mangled arguments. */
|
package/dist/grants.js
CHANGED
|
@@ -39,6 +39,19 @@ export const WRITE_CAPABILITIES = {
|
|
|
39
39
|
// See docs/decisions/0003-onenote-writes.md.
|
|
40
40
|
'onenote-create': 'add a new page to a OneNote section, after showing it to you first. ' +
|
|
41
41
|
'It cannot change or delete any page, including the ones it made.',
|
|
42
|
+
// Also not this file's boundary to keep, and for a stronger reason than
|
|
43
|
+
// `onenote-create`: `Notes.ReadWrite.CreatedByApp` distinguishes *this* app
|
|
44
|
+
// from every other, so a page the musician wrote is refused by Microsoft
|
|
45
|
+
// before our code is reached — verified as 401 40003, not assumed.
|
|
46
|
+
//
|
|
47
|
+
// One capability covering both actions rather than two. No scope separates
|
|
48
|
+
// appending from replacing, so a second grant would be a promise this
|
|
49
|
+
// repository keeps while reading like the provider-enforced kind, which is
|
|
50
|
+
// the confusion 0003 exists to prevent. What makes a replace survivable is
|
|
51
|
+
// the pre-image, not the name it was granted under.
|
|
52
|
+
// See docs/decisions/0004-onenote-page-maintenance.md.
|
|
53
|
+
'onenote-edit': 'change a page that artist-mcp itself created, after showing it to you first. ' +
|
|
54
|
+
'It cannot touch a page you wrote, and it cannot delete anything.',
|
|
42
55
|
};
|
|
43
56
|
/** Whether a bare token names a capability, for untangling mangled arguments. */
|
|
44
57
|
export const isWriteCapability = (value) => typeof value === 'string' && Object.keys(WRITE_CAPABILITIES).includes(value);
|
package/dist/oauth.js
CHANGED
|
@@ -157,6 +157,18 @@ export const WRITE_SCOPES = {
|
|
|
157
157
|
// Microsoft. Asking for `Notes.ReadWrite` instead would hand back exactly
|
|
158
158
|
// the position 0001 was stuck in. See docs/decisions/0003-onenote-writes.md.
|
|
159
159
|
'onenote-create': { provider: 'microsoft', scopes: ['Notes.Create'] },
|
|
160
|
+
// Named in the body of Microsoft's own 403 for PATCH and DELETE, and absent
|
|
161
|
+
// from the documentation for the operations it enables — the permissions
|
|
162
|
+
// table for page-delete lists only Notes.ReadWrite and Notes.ReadWrite.All,
|
|
163
|
+
// and says higher privileges are "not available" for a personal account,
|
|
164
|
+
// while a personal account edits with this one. Treat that reference as
|
|
165
|
+
// describing intent and probe for behaviour; scripts/spike-onenote-patch.mjs
|
|
166
|
+
// is that probe.
|
|
167
|
+
//
|
|
168
|
+
// Enforced per-app, not per-scope-holder: a page another application created
|
|
169
|
+
// is refused with 403 40006, and one the musician wrote with 401 40003.
|
|
170
|
+
// Consent for it folds in Notes.Create unasked, observed on every run.
|
|
171
|
+
'onenote-edit': { provider: 'microsoft', scopes: ['Notes.ReadWrite.CreatedByApp'] },
|
|
160
172
|
};
|
|
161
173
|
/**
|
|
162
174
|
* What to ask consent for, given what this install was granted.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editing a page this tool created, and only such a page.
|
|
3
|
+
*
|
|
4
|
+
* Like `onenote-write.ts`, there is no ownership boundary in this file, and for
|
|
5
|
+
* the same reason: `Notes.ReadWrite.CreatedByApp` refuses a page the musician
|
|
6
|
+
* wrote before our code is consulted — verified as `401 40003` against a real
|
|
7
|
+
* notebook. What *is* here is the machinery that makes a replace permissible at
|
|
8
|
+
* all, which is a different question and ours to get right. See
|
|
9
|
+
* docs/decisions/0004-onenote-page-maintenance.md.
|
|
10
|
+
*
|
|
11
|
+
* The rule that record rests on: **a replace may not proceed unless the content
|
|
12
|
+
* it is about to destroy has been captured first.** OneNote keeps no version and
|
|
13
|
+
* no recycle bin, so the pre-image is the only undo that exists. It is not a
|
|
14
|
+
* feature attached to replacing; it is the reason replacing is allowed.
|
|
15
|
+
*
|
|
16
|
+
* Three facts about Graph's patch API are encoded below rather than remembered,
|
|
17
|
+
* because two probe runs concluded the opposite of the truth from them:
|
|
18
|
+
*
|
|
19
|
+
* 1. A `data-id` target must carry a `#`. Without it the target does not
|
|
20
|
+
* resolve and Graph answers `20134`, which reads like "this element cannot
|
|
21
|
+
* be edited" and means "there is no such target".
|
|
22
|
+
* 2. `20138` is the *other* thing — the element resolved and does not support
|
|
23
|
+
* that action. A paragraph returns it for `append` and always will, which
|
|
24
|
+
* is documented behaviour and not a sign anything is wrong.
|
|
25
|
+
* 3. Replace requires the **generated** id, never a `data-id`, for everything
|
|
26
|
+
* except the title and images inside a div. Generated ids are returned only
|
|
27
|
+
* with `includeIDs=true`, and Microsoft warns they "might change after a
|
|
28
|
+
* page update" — so they are read immediately before the command that uses
|
|
29
|
+
* them, never carried across a write.
|
|
30
|
+
*/
|
|
31
|
+
import { type RecordWrite } from './audit.js';
|
|
32
|
+
export declare const isGeneratedId: (id: string) => boolean;
|
|
33
|
+
export type PageTargets = {
|
|
34
|
+
/** Author-supplied, addressed with a leading `#`. Usable for append only. */
|
|
35
|
+
dataIds: string[];
|
|
36
|
+
/** Graph-generated, addressed bare. Required for replace. */
|
|
37
|
+
generatedIds: string[];
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* What the page currently offers as targets.
|
|
41
|
+
*
|
|
42
|
+
* Read from the output of `GET .../content?includeIDs=true`. Both attributes
|
|
43
|
+
* are collected because they are not interchangeable: `data-id` is ours and
|
|
44
|
+
* survives a round trip, `id` is Graph's and moves. A caller wanting to replace
|
|
45
|
+
* needs the second even when it labelled the element with the first.
|
|
46
|
+
*/
|
|
47
|
+
export declare const readTargets: (html: string) => PageTargets;
|
|
48
|
+
/**
|
|
49
|
+
* Cut one element, with its children, out of the page HTML.
|
|
50
|
+
*
|
|
51
|
+
* Depth-counted rather than matched with a lazy regex. A non-greedy match to
|
|
52
|
+
* the first closing tag returns a fragment whenever the target has children,
|
|
53
|
+
* and a fragment restores as silent truncation — which is the precise failure
|
|
54
|
+
* a pre-image exists to prevent. Returns undefined rather than guessing when
|
|
55
|
+
* the markup does not balance.
|
|
56
|
+
*/
|
|
57
|
+
export declare const extractElement: (html: string, attr: "id" | "data-id", value: string) => string | undefined;
|
|
58
|
+
/**
|
|
59
|
+
* The content a replace is about to destroy, or a refusal.
|
|
60
|
+
*
|
|
61
|
+
* Throws rather than returning undefined, because every caller must treat a
|
|
62
|
+
* failed capture as a reason not to write. Making that the return type would
|
|
63
|
+
* let a caller carry on with `?? ''` and destroy a paragraph with nothing held
|
|
64
|
+
* back, which is exactly the outcome 0004 forbids.
|
|
65
|
+
*/
|
|
66
|
+
export declare const preImage: (html: string, generatedId: string) => string;
|
|
67
|
+
export type PatchCommand = {
|
|
68
|
+
target: string;
|
|
69
|
+
action: 'append' | 'replace';
|
|
70
|
+
content: string;
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Append to a container.
|
|
74
|
+
*
|
|
75
|
+
* `body` and `title` are keywords and take no prefix; everything else appended
|
|
76
|
+
* to is addressed by `data-id`, which does. Getting this wrong is not a visible
|
|
77
|
+
* error — it is a `20134` that reads as though the element were uneditable.
|
|
78
|
+
*/
|
|
79
|
+
export declare const appendCommand: (target: string, contentHtml: string) => PatchCommand;
|
|
80
|
+
/**
|
|
81
|
+
* Replace one element, named by its generated id.
|
|
82
|
+
*
|
|
83
|
+
* `data-id` is refused rather than translated. Graph accepts a `data-id` for a
|
|
84
|
+
* replace on images and objects only, and silently doing the wrong thing for
|
|
85
|
+
* every other element would mean a command that looks right and either fails
|
|
86
|
+
* confusingly or hits something other than what was meant.
|
|
87
|
+
*/
|
|
88
|
+
export declare const replaceCommand: (generatedId: string, contentHtml: string) => PatchCommand;
|
|
89
|
+
/**
|
|
90
|
+
* Plain text as a single paragraph, which is what replaces a paragraph.
|
|
91
|
+
*
|
|
92
|
+
* A replace must yield an element of a kind the target supports, so this is
|
|
93
|
+
* deliberately not general: one `<p>`, with line breaks preserved and nothing
|
|
94
|
+
* else interpreted. Anything richer is a different operation and should be
|
|
95
|
+
* decided rather than smuggled in through the content string.
|
|
96
|
+
*/
|
|
97
|
+
export declare const paragraph: (text: string) => string;
|
|
98
|
+
export type EditDraft = {
|
|
99
|
+
page_id: string;
|
|
100
|
+
action: 'append' | 'replace';
|
|
101
|
+
/** The generated id of the element to replace. Absent for an append. */
|
|
102
|
+
element_id: string | null;
|
|
103
|
+
text: string;
|
|
104
|
+
source_page: string | null;
|
|
105
|
+
};
|
|
106
|
+
export declare const editFrom: (params: Record<string, unknown>) => EditDraft;
|
|
107
|
+
/**
|
|
108
|
+
* The token a preview returns and an edit must carry back.
|
|
109
|
+
*
|
|
110
|
+
* Derived from the payload **and from what the change would destroy**, which is
|
|
111
|
+
* the one difference from the create token and the reason this is not shared
|
|
112
|
+
* with `onenote-write.ts`. A create can only be wrong about what it writes. A
|
|
113
|
+
* replace can also be wrong about what it overwrites — and the id naming that
|
|
114
|
+
* content moves after every page update, so an id that was correct at preview
|
|
115
|
+
* may point at something else by the time the edit is applied.
|
|
116
|
+
*
|
|
117
|
+
* Hashing the pre-image makes that case fail closed: the edit is refused unless
|
|
118
|
+
* the element still holds exactly what the musician was shown.
|
|
119
|
+
*/
|
|
120
|
+
export declare const editToken: (draft: EditDraft, preImageHtml: string) => Promise<string>;
|
|
121
|
+
/**
|
|
122
|
+
* What the page currently says, element by element, so a caller can name the
|
|
123
|
+
* part to change without guessing an id.
|
|
124
|
+
*
|
|
125
|
+
* Every id here is read in this call and is only good for the next one. They
|
|
126
|
+
* are not stable identifiers and must never be stored, handed to a musician, or
|
|
127
|
+
* quoted back in a later session — Microsoft says they "might change after a
|
|
128
|
+
* page update", and that was observed rather than merely read.
|
|
129
|
+
*/
|
|
130
|
+
export declare const readEditableParts: (token: string, pageId: string) => Promise<{
|
|
131
|
+
html: string;
|
|
132
|
+
etag: string | null;
|
|
133
|
+
parts: {
|
|
134
|
+
element_id: string;
|
|
135
|
+
text: string;
|
|
136
|
+
}[];
|
|
137
|
+
}>;
|
|
138
|
+
/**
|
|
139
|
+
* Show the change that would be made, and hand back the token making it needs.
|
|
140
|
+
*
|
|
141
|
+
* A read despite the name, exactly as the calendar and create previews are.
|
|
142
|
+
*/
|
|
143
|
+
export declare function previewEdit(token: string, params: Record<string, unknown>): Promise<{
|
|
144
|
+
preview: null;
|
|
145
|
+
page_id: string;
|
|
146
|
+
parts: {
|
|
147
|
+
element_id: string;
|
|
148
|
+
text: string;
|
|
149
|
+
}[];
|
|
150
|
+
confirmation_token: null;
|
|
151
|
+
note: string;
|
|
152
|
+
} | {
|
|
153
|
+
preview: string;
|
|
154
|
+
page_id: string;
|
|
155
|
+
parts: {
|
|
156
|
+
element_id: string;
|
|
157
|
+
text: string;
|
|
158
|
+
}[];
|
|
159
|
+
confirmation_token: string;
|
|
160
|
+
note: string;
|
|
161
|
+
}>;
|
|
162
|
+
/**
|
|
163
|
+
* Apply the change, having shown it.
|
|
164
|
+
*
|
|
165
|
+
* The page is read again here rather than trusted from the preview, and that is
|
|
166
|
+
* not caution: generated ids move after any page update, so an id that was
|
|
167
|
+
* right at preview may name something else by now. Re-reading also produces the
|
|
168
|
+
* ETag that goes out as `If-Match`, which is what makes the musician typing in
|
|
169
|
+
* OneNote between the two calls a refusal rather than a silent overwrite.
|
|
170
|
+
*
|
|
171
|
+
* The confirmation token is checked against the payload *and* the pre-image, so
|
|
172
|
+
* an element whose content changed since the preview fails closed.
|
|
173
|
+
*/
|
|
174
|
+
export declare function applyEdit(token: string, params: Record<string, unknown>, record: RecordWrite): Promise<{
|
|
175
|
+
changed: boolean;
|
|
176
|
+
page_id: string;
|
|
177
|
+
note: string;
|
|
178
|
+
}>;
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editing a page this tool created, and only such a page.
|
|
3
|
+
*
|
|
4
|
+
* Like `onenote-write.ts`, there is no ownership boundary in this file, and for
|
|
5
|
+
* the same reason: `Notes.ReadWrite.CreatedByApp` refuses a page the musician
|
|
6
|
+
* wrote before our code is consulted — verified as `401 40003` against a real
|
|
7
|
+
* notebook. What *is* here is the machinery that makes a replace permissible at
|
|
8
|
+
* all, which is a different question and ours to get right. See
|
|
9
|
+
* docs/decisions/0004-onenote-page-maintenance.md.
|
|
10
|
+
*
|
|
11
|
+
* The rule that record rests on: **a replace may not proceed unless the content
|
|
12
|
+
* it is about to destroy has been captured first.** OneNote keeps no version and
|
|
13
|
+
* no recycle bin, so the pre-image is the only undo that exists. It is not a
|
|
14
|
+
* feature attached to replacing; it is the reason replacing is allowed.
|
|
15
|
+
*
|
|
16
|
+
* Three facts about Graph's patch API are encoded below rather than remembered,
|
|
17
|
+
* because two probe runs concluded the opposite of the truth from them:
|
|
18
|
+
*
|
|
19
|
+
* 1. A `data-id` target must carry a `#`. Without it the target does not
|
|
20
|
+
* resolve and Graph answers `20134`, which reads like "this element cannot
|
|
21
|
+
* be edited" and means "there is no such target".
|
|
22
|
+
* 2. `20138` is the *other* thing — the element resolved and does not support
|
|
23
|
+
* that action. A paragraph returns it for `append` and always will, which
|
|
24
|
+
* is documented behaviour and not a sign anything is wrong.
|
|
25
|
+
* 3. Replace requires the **generated** id, never a `data-id`, for everything
|
|
26
|
+
* except the title and images inside a div. Generated ids are returned only
|
|
27
|
+
* with `includeIDs=true`, and Microsoft warns they "might change after a
|
|
28
|
+
* page update" — so they are read immediately before the command that uses
|
|
29
|
+
* them, never carried across a write.
|
|
30
|
+
*/
|
|
31
|
+
import { onenotePageContent, onenotePatchPage } from './api.js';
|
|
32
|
+
import { GraphError } from './client.js';
|
|
33
|
+
const failure = (message) => new GraphError(message, false);
|
|
34
|
+
/**
|
|
35
|
+
* A generated id as Graph emits it: `p:{guid}{40}`, and for a div nested in a
|
|
36
|
+
* div, the doubled `div:{guid}{42}:{guid}{45}`.
|
|
37
|
+
*
|
|
38
|
+
* Shape-checked because it is concatenated into a JSON command that Graph acts
|
|
39
|
+
* on, and because it is the one value here that decides what gets destroyed. A
|
|
40
|
+
* caller that has invented an id, or kept one across a write, should be refused
|
|
41
|
+
* rather than allowed to overwrite whatever that id now points at.
|
|
42
|
+
*/
|
|
43
|
+
const GENERATED_ID = /^(?:p|div|ol|ul|li|table|img|object|h[1-6]):\{[0-9a-fA-F-]{36}\}\{\d+\}(?::\{[0-9a-fA-F-]{36}\}\{\d+\})?$/;
|
|
44
|
+
export const isGeneratedId = (id) => GENERATED_ID.test(id);
|
|
45
|
+
/**
|
|
46
|
+
* What the page currently offers as targets.
|
|
47
|
+
*
|
|
48
|
+
* Read from the output of `GET .../content?includeIDs=true`. Both attributes
|
|
49
|
+
* are collected because they are not interchangeable: `data-id` is ours and
|
|
50
|
+
* survives a round trip, `id` is Graph's and moves. A caller wanting to replace
|
|
51
|
+
* needs the second even when it labelled the element with the first.
|
|
52
|
+
*/
|
|
53
|
+
export const readTargets = (html) => ({
|
|
54
|
+
dataIds: [...html.matchAll(/\sdata-id="([^"]+)"/g)].map((m) => m[1]),
|
|
55
|
+
generatedIds: [...html.matchAll(/\sid="([^"]+)"/g)]
|
|
56
|
+
.map((m) => m[1])
|
|
57
|
+
.filter((id) => isGeneratedId(id)),
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Cut one element, with its children, out of the page HTML.
|
|
61
|
+
*
|
|
62
|
+
* Depth-counted rather than matched with a lazy regex. A non-greedy match to
|
|
63
|
+
* the first closing tag returns a fragment whenever the target has children,
|
|
64
|
+
* and a fragment restores as silent truncation — which is the precise failure
|
|
65
|
+
* a pre-image exists to prevent. Returns undefined rather than guessing when
|
|
66
|
+
* the markup does not balance.
|
|
67
|
+
*/
|
|
68
|
+
export const extractElement = (html, attr, value) => {
|
|
69
|
+
const quoted = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
70
|
+
const open = new RegExp(`<([a-z][a-z0-9]*)\\b[^>]*\\b${attr}="${quoted}"[^>]*>`, 'i').exec(html);
|
|
71
|
+
if (open === null)
|
|
72
|
+
return undefined;
|
|
73
|
+
const tag = open[1];
|
|
74
|
+
const step = new RegExp(`<(/?)${tag}\\b[^>]*?(/?)>`, 'gi');
|
|
75
|
+
step.lastIndex = open.index;
|
|
76
|
+
let depth = 0;
|
|
77
|
+
let match;
|
|
78
|
+
while ((match = step.exec(html)) !== null) {
|
|
79
|
+
// A self-closing tag enters and leaves in one token.
|
|
80
|
+
if (match[2] === '/')
|
|
81
|
+
continue;
|
|
82
|
+
depth += match[1] === '/' ? -1 : 1;
|
|
83
|
+
if (depth === 0)
|
|
84
|
+
return html.slice(open.index, match.index + match[0].length);
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The content a replace is about to destroy, or a refusal.
|
|
90
|
+
*
|
|
91
|
+
* Throws rather than returning undefined, because every caller must treat a
|
|
92
|
+
* failed capture as a reason not to write. Making that the return type would
|
|
93
|
+
* let a caller carry on with `?? ''` and destroy a paragraph with nothing held
|
|
94
|
+
* back, which is exactly the outcome 0004 forbids.
|
|
95
|
+
*/
|
|
96
|
+
export const preImage = (html, generatedId) => {
|
|
97
|
+
if (!isGeneratedId(generatedId)) {
|
|
98
|
+
throw failure(`"${generatedId}" is not a generated element id. A replace has to name one, ` +
|
|
99
|
+
'read from the page immediately beforehand.');
|
|
100
|
+
}
|
|
101
|
+
const captured = extractElement(html, 'id', generatedId);
|
|
102
|
+
if (captured === undefined) {
|
|
103
|
+
throw failure('The element to be replaced could not be read back from the page, so what ' +
|
|
104
|
+
'the change would overwrite cannot be captured. Nothing was changed. ' +
|
|
105
|
+
'OneNote keeps no version of a page, so a replace without that capture ' +
|
|
106
|
+
'would be unrecoverable.');
|
|
107
|
+
}
|
|
108
|
+
return captured;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Append to a container.
|
|
112
|
+
*
|
|
113
|
+
* `body` and `title` are keywords and take no prefix; everything else appended
|
|
114
|
+
* to is addressed by `data-id`, which does. Getting this wrong is not a visible
|
|
115
|
+
* error — it is a `20134` that reads as though the element were uneditable.
|
|
116
|
+
*/
|
|
117
|
+
export const appendCommand = (target, contentHtml) => {
|
|
118
|
+
if (target === 'title') {
|
|
119
|
+
throw failure('A title cannot be appended to; it can only be replaced.');
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
target: target === 'body' || isGeneratedId(target) ? target : `#${target.replace(/^#/, '')}`,
|
|
123
|
+
action: 'append',
|
|
124
|
+
content: contentHtml,
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Replace one element, named by its generated id.
|
|
129
|
+
*
|
|
130
|
+
* `data-id` is refused rather than translated. Graph accepts a `data-id` for a
|
|
131
|
+
* replace on images and objects only, and silently doing the wrong thing for
|
|
132
|
+
* every other element would mean a command that looks right and either fails
|
|
133
|
+
* confusingly or hits something other than what was meant.
|
|
134
|
+
*/
|
|
135
|
+
export const replaceCommand = (generatedId, contentHtml) => {
|
|
136
|
+
if (!isGeneratedId(generatedId)) {
|
|
137
|
+
throw failure(`A replace must name a generated element id, not "${generatedId}". Read the ` +
|
|
138
|
+
'page with includeIDs=true and use the id it reports for the element.');
|
|
139
|
+
}
|
|
140
|
+
return { target: generatedId, action: 'replace', content: contentHtml };
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* XHTML entities, as in onenote-write.ts and for the same reason: a model
|
|
144
|
+
* composing a correction from an email is passing through text it did not
|
|
145
|
+
* author, and unescaped markup would let that text decide what the page says.
|
|
146
|
+
*/
|
|
147
|
+
const escapeXml = (value) => value
|
|
148
|
+
.replace(/&/g, '&')
|
|
149
|
+
.replace(/</g, '<')
|
|
150
|
+
.replace(/>/g, '>')
|
|
151
|
+
.replace(/"/g, '"')
|
|
152
|
+
.replace(/'/g, ''');
|
|
153
|
+
/**
|
|
154
|
+
* Plain text as a single paragraph, which is what replaces a paragraph.
|
|
155
|
+
*
|
|
156
|
+
* A replace must yield an element of a kind the target supports, so this is
|
|
157
|
+
* deliberately not general: one `<p>`, with line breaks preserved and nothing
|
|
158
|
+
* else interpreted. Anything richer is a different operation and should be
|
|
159
|
+
* decided rather than smuggled in through the content string.
|
|
160
|
+
*/
|
|
161
|
+
export const paragraph = (text) => {
|
|
162
|
+
const trimmed = text.trim();
|
|
163
|
+
if (trimmed === '')
|
|
164
|
+
throw failure('There is no text to write.');
|
|
165
|
+
return `<p>${escapeXml(trimmed).replace(/\n/g, '<br/>')}</p>`;
|
|
166
|
+
};
|
|
167
|
+
/** As in onenote-write.ts: opaque, but concatenated into URLs. */
|
|
168
|
+
const ONENOTE_ID = /^[A-Za-z0-9!._~-]{1,300}$/;
|
|
169
|
+
const MAX_TEXT_CHARS = 4_000;
|
|
170
|
+
export const editFrom = (params) => {
|
|
171
|
+
const pageId = params.page_id;
|
|
172
|
+
if (typeof pageId !== 'string' || !ONENOTE_ID.test(pageId)) {
|
|
173
|
+
throw failure('page_id is required, and must name the page to change.');
|
|
174
|
+
}
|
|
175
|
+
const action = params.action;
|
|
176
|
+
if (action !== 'append' && action !== 'replace') {
|
|
177
|
+
throw failure("action must be either 'append' or 'replace'. Nothing else can be done to a page.");
|
|
178
|
+
}
|
|
179
|
+
const text = params.text;
|
|
180
|
+
if (typeof text !== 'string' || text.trim() === '') {
|
|
181
|
+
throw failure('text is required: there is nothing to write.');
|
|
182
|
+
}
|
|
183
|
+
if (text.length > MAX_TEXT_CHARS) {
|
|
184
|
+
throw failure(`That is ${text.length} characters. An edit records a decision, not a page — ` +
|
|
185
|
+
`${MAX_TEXT_CHARS} is the most. A longer change is a new page.`);
|
|
186
|
+
}
|
|
187
|
+
const elementId = params.element_id;
|
|
188
|
+
if (action === 'replace') {
|
|
189
|
+
if (typeof elementId !== 'string' || !isGeneratedId(elementId)) {
|
|
190
|
+
throw failure('A replace needs element_id: the generated id of the part to change, as ' +
|
|
191
|
+
'preview_onenote_edit reports it. Read the page again if you do not have one.');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (elementId !== undefined && elementId !== null && elementId !== '') {
|
|
195
|
+
throw failure('An append goes at the end of the page and takes no element_id. To change ' +
|
|
196
|
+
'something already written, use replace.');
|
|
197
|
+
}
|
|
198
|
+
const source = params.source_page;
|
|
199
|
+
if (source !== undefined && source !== null && source !== '') {
|
|
200
|
+
if (typeof source !== 'string' || !ONENOTE_ID.test(source)) {
|
|
201
|
+
throw failure('source_page is malformed.');
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
page_id: pageId,
|
|
206
|
+
action,
|
|
207
|
+
element_id: action === 'replace' ? elementId : null,
|
|
208
|
+
text,
|
|
209
|
+
source_page: typeof source === 'string' && source !== '' ? source : null,
|
|
210
|
+
};
|
|
211
|
+
};
|
|
212
|
+
const base32hex = (bytes) => {
|
|
213
|
+
const alphabet = '0123456789abcdefghijklmnopqrstuv';
|
|
214
|
+
let bits = 0;
|
|
215
|
+
let value = 0;
|
|
216
|
+
let out = '';
|
|
217
|
+
for (const byte of bytes) {
|
|
218
|
+
value = (value << 8) | byte;
|
|
219
|
+
bits += 8;
|
|
220
|
+
while (bits >= 5) {
|
|
221
|
+
out += alphabet[(value >>> (bits - 5)) & 31];
|
|
222
|
+
bits -= 5;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* The token a preview returns and an edit must carry back.
|
|
229
|
+
*
|
|
230
|
+
* Derived from the payload **and from what the change would destroy**, which is
|
|
231
|
+
* the one difference from the create token and the reason this is not shared
|
|
232
|
+
* with `onenote-write.ts`. A create can only be wrong about what it writes. A
|
|
233
|
+
* replace can also be wrong about what it overwrites — and the id naming that
|
|
234
|
+
* content moves after every page update, so an id that was correct at preview
|
|
235
|
+
* may point at something else by the time the edit is applied.
|
|
236
|
+
*
|
|
237
|
+
* Hashing the pre-image makes that case fail closed: the edit is refused unless
|
|
238
|
+
* the element still holds exactly what the musician was shown.
|
|
239
|
+
*/
|
|
240
|
+
export const editToken = async (draft, preImageHtml) => {
|
|
241
|
+
const canonical = [draft.page_id, draft.action, draft.element_id ?? '', draft.text].join(' ');
|
|
242
|
+
const data = new TextEncoder().encode(`confirm-edit ${canonical} ${preImageHtml}`);
|
|
243
|
+
return base32hex(new Uint8Array(await crypto.subtle.digest('SHA-256', data)));
|
|
244
|
+
};
|
|
245
|
+
/**
|
|
246
|
+
* Strip tags for display. The page is shown as the musician reads it, not as
|
|
247
|
+
* HTML — and this text is what they approve a destructive change against, so
|
|
248
|
+
* markup leaking into it is not cosmetic.
|
|
249
|
+
*
|
|
250
|
+
* Numeric entities are decoded as well as named ones. We write `'`, but
|
|
251
|
+
* OneNote stores an apostrophe as `'` and hands it back that way, so a
|
|
252
|
+
* decoder built only from what this file emits showed a preview reading
|
|
253
|
+
* "the product's own tool path" — observed on a real page.
|
|
254
|
+
*
|
|
255
|
+
* Ampersand last, or the entities the earlier passes produce get decoded again.
|
|
256
|
+
*/
|
|
257
|
+
const asText = (html) => html
|
|
258
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
259
|
+
.replace(/<[^>]+>/g, '')
|
|
260
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCodePoint(parseInt(hex, 16)))
|
|
261
|
+
.replace(/&#(\d+);/g, (_, dec) => String.fromCodePoint(Number(dec)))
|
|
262
|
+
.replace(/</g, '<')
|
|
263
|
+
.replace(/>/g, '>')
|
|
264
|
+
.replace(/"/g, '"')
|
|
265
|
+
.replace(/'/g, "'")
|
|
266
|
+
.replace(/ /g, ' ')
|
|
267
|
+
.replace(/&/g, '&')
|
|
268
|
+
.trim();
|
|
269
|
+
/**
|
|
270
|
+
* What the page currently says, element by element, so a caller can name the
|
|
271
|
+
* part to change without guessing an id.
|
|
272
|
+
*
|
|
273
|
+
* Every id here is read in this call and is only good for the next one. They
|
|
274
|
+
* are not stable identifiers and must never be stored, handed to a musician, or
|
|
275
|
+
* quoted back in a later session — Microsoft says they "might change after a
|
|
276
|
+
* page update", and that was observed rather than merely read.
|
|
277
|
+
*/
|
|
278
|
+
export const readEditableParts = async (token, pageId) => {
|
|
279
|
+
const { html, etag } = await onenotePageContent(pageId, token);
|
|
280
|
+
const parts = readTargets(html)
|
|
281
|
+
.generatedIds.filter((id) => /^(?:p|h[1-6]|li):/.test(id))
|
|
282
|
+
.map((id) => {
|
|
283
|
+
const element = extractElement(html, 'id', id);
|
|
284
|
+
return { element_id: id, text: element === undefined ? '' : asText(element) };
|
|
285
|
+
})
|
|
286
|
+
.filter((part) => part.text !== '');
|
|
287
|
+
return { html, etag, parts };
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Show the change that would be made, and hand back the token making it needs.
|
|
291
|
+
*
|
|
292
|
+
* A read despite the name, exactly as the calendar and create previews are.
|
|
293
|
+
*/
|
|
294
|
+
export async function previewEdit(token, params) {
|
|
295
|
+
// Naming a part to replace requires knowing its id, and an id is only
|
|
296
|
+
// knowable by reading the page. So a replace with no element_id is not a
|
|
297
|
+
// malformed call — it is the first half of the only workflow available, and
|
|
298
|
+
// the tool text says so. It used to be refused here, which meant a model
|
|
299
|
+
// following that text got an error and had to guess its way out.
|
|
300
|
+
//
|
|
301
|
+
// No confirmation token comes back from this branch. Nothing has been chosen
|
|
302
|
+
// yet, and a token would be one for a change nobody has described.
|
|
303
|
+
const wantsPart = params.action === 'replace' &&
|
|
304
|
+
(params.element_id === undefined || params.element_id === null || params.element_id === '');
|
|
305
|
+
if (wantsPart) {
|
|
306
|
+
const pageId = params.page_id;
|
|
307
|
+
if (typeof pageId !== 'string' || !ONENOTE_ID.test(pageId)) {
|
|
308
|
+
throw failure('page_id is required, and must name the page to change.');
|
|
309
|
+
}
|
|
310
|
+
const { parts } = await readEditableParts(token, pageId);
|
|
311
|
+
return {
|
|
312
|
+
preview: null,
|
|
313
|
+
page_id: pageId,
|
|
314
|
+
parts,
|
|
315
|
+
confirmation_token: null,
|
|
316
|
+
note: parts.length === 0
|
|
317
|
+
? 'This page has no separately editable parts, so nothing on it can be ' +
|
|
318
|
+
'replaced. An append can still add to the end of it.'
|
|
319
|
+
: 'Nothing has been previewed yet. These are the parts of the page as it ' +
|
|
320
|
+
'stands. Name one as element_id and preview again to see the change ' +
|
|
321
|
+
'itself. The ids were read just now and are good only for the next call.',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const draft = editFrom(params);
|
|
325
|
+
const { html, parts } = await readEditableParts(token, draft.page_id);
|
|
326
|
+
if (draft.action === 'append') {
|
|
327
|
+
return {
|
|
328
|
+
preview: `Add to the end of the page:\n\n${draft.text.trim()}`,
|
|
329
|
+
page_id: draft.page_id,
|
|
330
|
+
parts,
|
|
331
|
+
confirmation_token: await editToken(draft, ''),
|
|
332
|
+
note: 'Show this to the musician and wait for their yes. Nothing on the page is ' +
|
|
333
|
+
'changed or removed by an append — it adds to the end.',
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const captured = preImage(html, draft.element_id);
|
|
337
|
+
return {
|
|
338
|
+
preview: `Replace this:\n\n${asText(captured)}\n\nWith this:\n\n${draft.text.trim()}`,
|
|
339
|
+
page_id: draft.page_id,
|
|
340
|
+
parts,
|
|
341
|
+
confirmation_token: await editToken(draft, captured),
|
|
342
|
+
note: 'Show this to the musician and wait for their yes. This overwrites what is ' +
|
|
343
|
+
'quoted above, and OneNote keeps no version of a page — what it replaces is ' +
|
|
344
|
+
'kept in this install\'s own write log and nowhere else.',
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Apply the change, having shown it.
|
|
349
|
+
*
|
|
350
|
+
* The page is read again here rather than trusted from the preview, and that is
|
|
351
|
+
* not caution: generated ids move after any page update, so an id that was
|
|
352
|
+
* right at preview may name something else by now. Re-reading also produces the
|
|
353
|
+
* ETag that goes out as `If-Match`, which is what makes the musician typing in
|
|
354
|
+
* OneNote between the two calls a refusal rather than a silent overwrite.
|
|
355
|
+
*
|
|
356
|
+
* The confirmation token is checked against the payload *and* the pre-image, so
|
|
357
|
+
* an element whose content changed since the preview fails closed.
|
|
358
|
+
*/
|
|
359
|
+
export async function applyEdit(token, params, record) {
|
|
360
|
+
const draft = editFrom(params);
|
|
361
|
+
const { html, etag } = await onenotePageContent(draft.page_id, token);
|
|
362
|
+
const captured = draft.action === 'replace' ? preImage(html, draft.element_id) : '';
|
|
363
|
+
const expected = await editToken(draft, captured);
|
|
364
|
+
if (params.confirmation_token !== expected) {
|
|
365
|
+
throw failure(draft.action === 'replace'
|
|
366
|
+
? 'That part of the page is not what was previewed — either the values ' +
|
|
367
|
+
'changed, or the page did since it was shown. Nothing was written. Call ' +
|
|
368
|
+
'preview_onenote_edit again and show the musician what it returns now.'
|
|
369
|
+
: 'The confirmation token does not match these values. Call ' +
|
|
370
|
+
'preview_onenote_edit again with exactly what should be written, show the ' +
|
|
371
|
+
'musician what it returns, then apply it with those same values.');
|
|
372
|
+
}
|
|
373
|
+
const command = draft.action === 'append'
|
|
374
|
+
? appendCommand('body', paragraph(draft.text))
|
|
375
|
+
: replaceCommand(draft.element_id, paragraph(draft.text));
|
|
376
|
+
await onenotePatchPage(draft.page_id, [command], token, etag);
|
|
377
|
+
// After the write, and carrying what it destroyed. For a replace this line is
|
|
378
|
+
// the only copy of the previous content that exists anywhere.
|
|
379
|
+
await record({
|
|
380
|
+
operation: draft.action === 'append' ? 'append_onenote_page' : 'replace_onenote_element',
|
|
381
|
+
// The text itself, not merely that something happened. `summary` is
|
|
382
|
+
// documented as what was written "as the user would read it back", and the
|
|
383
|
+
// calendar rows honour that while these two originally named only the page
|
|
384
|
+
// id — so the log recorded the replaced text in `pre_image` and left the
|
|
385
|
+
// replacing text nowhere at all. Someone reading this line back is
|
|
386
|
+
// reconstructing an edit they cannot see any other way.
|
|
387
|
+
summary: draft.action === 'append'
|
|
388
|
+
? `Added to the end of the page: ${draft.text.trim()}`
|
|
389
|
+
: `Replaced "${asText(captured)}" with "${draft.text.trim()}"`,
|
|
390
|
+
target: draft.page_id,
|
|
391
|
+
source_page: draft.source_page,
|
|
392
|
+
pre_image: draft.action === 'replace' ? captured : null,
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
changed: true,
|
|
396
|
+
page_id: draft.page_id,
|
|
397
|
+
note: draft.action === 'append'
|
|
398
|
+
? 'The page now carries this at the end. Nothing was removed.'
|
|
399
|
+
: 'The page now says this. What it said before is in this install\'s write ' +
|
|
400
|
+
'log and nowhere else — OneNote keeps no version of a page, so tell the ' +
|
|
401
|
+
'musician that rather than implying it can be undone in OneNote.',
|
|
402
|
+
};
|
|
403
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -164,7 +164,7 @@ const renderChangedSections = (chosen, notebook, since) => {
|
|
|
164
164
|
caveats.join("\n\n"),
|
|
165
165
|
].join("\n\n");
|
|
166
166
|
};
|
|
167
|
-
const serverVersion = '2.
|
|
167
|
+
const serverVersion = '2.1.0'; // x-release-please-version
|
|
168
168
|
const errorResult = (err) => {
|
|
169
169
|
const message = err instanceof GraphError ? err.message : `Unexpected error: ${err}`;
|
|
170
170
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -1480,7 +1480,11 @@ const createServer = async (call, grants = []) => {
|
|
|
1480
1480
|
"holds, not a policy it follows, so a page created by mistake stays " +
|
|
1481
1481
|
"until the musician removes it in OneNote themselves. There is no bulk " +
|
|
1482
1482
|
"form. It never changes an existing page, so it is not a way to add a " +
|
|
1483
|
-
"line to one:
|
|
1483
|
+
"line to one: " +
|
|
1484
|
+
(isGranted(grants, "onenote-edit")
|
|
1485
|
+
? "use preview_onenote_edit and edit_onenote_page for that. "
|
|
1486
|
+
: "paste that for the musician as always. ") +
|
|
1487
|
+
"Requires the " +
|
|
1484
1488
|
"confirmation_token from a preview of these exact values; change any " +
|
|
1485
1489
|
"field and the token stops matching, which means preview again and show " +
|
|
1486
1490
|
"the musician the new version. Never write a value the musician chose in " +
|
|
@@ -1522,6 +1526,117 @@ const createServer = async (call, grants = []) => {
|
|
|
1522
1526
|
}
|
|
1523
1527
|
});
|
|
1524
1528
|
}
|
|
1529
|
+
/**
|
|
1530
|
+
* Editing a page this tool wrote.
|
|
1531
|
+
*
|
|
1532
|
+
* Everything the create tools lean on inverts here. There, the strongest
|
|
1533
|
+
* thing the text could say was that a mistake is permanent, because a model
|
|
1534
|
+
* that believes a mistake is repairable creates more freely. Here a mistake
|
|
1535
|
+
* *is* repairable, and the danger runs the other way: a model that believes
|
|
1536
|
+
* an edit is free will make more of them, and a replace destroys what it
|
|
1537
|
+
* overwrites with only this install's own write log standing behind it.
|
|
1538
|
+
*
|
|
1539
|
+
* So the text below says the opposite thing plainly — that OneNote keeps no
|
|
1540
|
+
* version, that the record of what was overwritten lives here and nowhere
|
|
1541
|
+
* else, and that a page the musician wrote is unreachable no matter how the
|
|
1542
|
+
* request is phrased. See docs/decisions/0004-onenote-page-maintenance.md.
|
|
1543
|
+
*/
|
|
1544
|
+
if (isGranted(grants, "onenote-edit")) {
|
|
1545
|
+
server.tool("preview_onenote_edit", "Call this before edit_onenote_page — it is the only way to obtain the confirmation_token that one requires, and it is what puts the exact change in front of the musician. SHOW THE MUSICIAN WHAT IT RETURNS AND WAIT FOR THEIR YES. " +
|
|
1546
|
+
"Reads the page as it stands right now and shows what the change would " +
|
|
1547
|
+
"do to it. Changes nothing. For a replace it quotes the text that would " +
|
|
1548
|
+
"be overwritten, which is the part the musician has to agree to. It also " +
|
|
1549
|
+
"returns the page's editable parts with an element_id for each, so pass " +
|
|
1550
|
+
"action 'replace' with no element_id first if you do not know which part " +
|
|
1551
|
+
"to change, read the parts back, then preview again naming one. Those " +
|
|
1552
|
+
"ids are read fresh each call and are good only for the next call — " +
|
|
1553
|
+
"never store one, repeat one to the musician, or reuse one from earlier " +
|
|
1554
|
+
"in the conversation.", {
|
|
1555
|
+
page_id: z
|
|
1556
|
+
.string()
|
|
1557
|
+
.describe("The page to change, by id from list_notes or read_note. The id, not the title"),
|
|
1558
|
+
action: z
|
|
1559
|
+
.enum(["append", "replace"])
|
|
1560
|
+
.describe("append adds a paragraph at the end and removes nothing. replace " +
|
|
1561
|
+
"overwrites one existing element and is the destructive one"),
|
|
1562
|
+
text: z.string().describe("The new text, plain. Markdown and HTML are NOT interpreted"),
|
|
1563
|
+
element_id: z
|
|
1564
|
+
.string()
|
|
1565
|
+
.optional()
|
|
1566
|
+
.describe("For replace only: which part to overwrite, using an element_id this " +
|
|
1567
|
+
"tool reported in the parts list on a previous call"),
|
|
1568
|
+
source_page: z
|
|
1569
|
+
.string()
|
|
1570
|
+
.optional()
|
|
1571
|
+
.describe("The page this change was decided from, so the write can be traced back"),
|
|
1572
|
+
}, async (params) => {
|
|
1573
|
+
try {
|
|
1574
|
+
const { preview, confirmation_token, parts, note } = await call("preview_onenote_edit", params);
|
|
1575
|
+
const listed = parts.length === 0
|
|
1576
|
+
? ""
|
|
1577
|
+
: "\n\nThe editable parts of this page right now:\n" +
|
|
1578
|
+
parts.map((p) => ` ${p.element_id}\n ${p.text}`).join("\n");
|
|
1579
|
+
// No token means nothing has been chosen yet: this was the call that
|
|
1580
|
+
// asks which part to change. Saying "wait for their yes" here would
|
|
1581
|
+
// be asking the musician to approve a change nobody has described.
|
|
1582
|
+
if (confirmation_token === null) {
|
|
1583
|
+
return { content: [{ type: "text", text: `${note}${listed}` }] };
|
|
1584
|
+
}
|
|
1585
|
+
return {
|
|
1586
|
+
content: [
|
|
1587
|
+
{
|
|
1588
|
+
type: "text",
|
|
1589
|
+
text: `${preview}${listed}\n\n` +
|
|
1590
|
+
"Show this to the musician and wait for their yes. If this is a " +
|
|
1591
|
+
"replace, say plainly what it overwrites: OneNote keeps no " +
|
|
1592
|
+
"version of a page, so the previous text will exist only in this " +
|
|
1593
|
+
"install's write log afterwards. If they agree, call " +
|
|
1594
|
+
"edit_onenote_page with the SAME values and:\n" +
|
|
1595
|
+
` confirmation_token: ${confirmation_token}`,
|
|
1596
|
+
},
|
|
1597
|
+
],
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
catch (err) {
|
|
1601
|
+
return errorResult(err);
|
|
1602
|
+
}
|
|
1603
|
+
});
|
|
1604
|
+
server.tool("edit_onenote_page", "Only call this after preview_onenote_edit AND after the musician has said yes to what the preview showed. Never call it to find out whether it would work. " +
|
|
1605
|
+
"Changes ONE page that artist-mcp itself created. It CANNOT touch a page " +
|
|
1606
|
+
"the musician wrote or one another app created — Microsoft refuses those, " +
|
|
1607
|
+
"not this tool — and it cannot delete anything. 'append' adds a paragraph " +
|
|
1608
|
+
"at the end and removes nothing. 'replace' overwrites one element and is " +
|
|
1609
|
+
"destructive: OneNote keeps no page version and a replaced paragraph is " +
|
|
1610
|
+
"recoverable only from this install's own write log, so never describe it " +
|
|
1611
|
+
"as undoable in OneNote. Requires the confirmation_token from a preview " +
|
|
1612
|
+
"of these exact values; if the page changed since that preview the token " +
|
|
1613
|
+
"stops matching and nothing is written, which means preview again and " +
|
|
1614
|
+
"show the musician the new version. Never write a value the musician " +
|
|
1615
|
+
"chose in chat to break a tie between pages that still disagree.", {
|
|
1616
|
+
page_id: z.string().describe("Exactly what the preview showed"),
|
|
1617
|
+
action: z.enum(["append", "replace"]).describe("Exactly what the preview showed"),
|
|
1618
|
+
text: z.string().describe("Exactly what the preview showed"),
|
|
1619
|
+
element_id: z
|
|
1620
|
+
.string()
|
|
1621
|
+
.optional()
|
|
1622
|
+
.describe("For replace: the same element_id the preview was given"),
|
|
1623
|
+
source_page: z
|
|
1624
|
+
.string()
|
|
1625
|
+
.optional()
|
|
1626
|
+
.describe("The page this change was decided from, so the write can be traced back"),
|
|
1627
|
+
confirmation_token: z
|
|
1628
|
+
.string()
|
|
1629
|
+
.describe("The token preview_onenote_edit returned for these exact values"),
|
|
1630
|
+
}, async (params) => {
|
|
1631
|
+
try {
|
|
1632
|
+
const { note } = await call("edit_onenote_page", params);
|
|
1633
|
+
return { content: [{ type: "text", text: note }] };
|
|
1634
|
+
}
|
|
1635
|
+
catch (err) {
|
|
1636
|
+
return errorResult(err);
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1525
1640
|
server.tool("list_calendars", "Call this before concluding that something is NOT in the calendar. A search of one calendar that finds nothing is not evidence of absence — it is evidence about one calendar. Gigs commonly sit on a band, venue or shared calendar rather than the primary one. " +
|
|
1526
1641
|
"Lists the Google calendars this musician has, with which one is primary " +
|
|
1527
1642
|
"and whether each is writable by them. Read-only: this never creates, " +
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@manudota/artist-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "MCP server that reads your OneNote notes, with optional Gmail and Calendar as supporting evidence, via a hosted connection.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|