@manudota/artist-mcp 1.5.2 → 1.5.3-staging.76

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/audit.d.ts CHANGED
@@ -12,6 +12,17 @@
12
12
  * since it defends against forgetting rather than against the user.
13
13
  */
14
14
  export declare const auditPath: () => string;
15
+ /**
16
+ * Where a completed write is recorded.
17
+ *
18
+ * Injected for the same reason the token resolver is: on this machine it is a
19
+ * file in the user's home directory, and hosted it is a row bound to one user.
20
+ * A file on a serverless host is one nobody will ever read, and "detectable and
21
+ * reversible" is the whole justification for these writes existing — so the
22
+ * sink has to be the caller's to choose. See
23
+ * docs/decisions/0002-hosted-writes.md.
24
+ */
25
+ export type RecordWrite = (entry: WriteRecord) => Promise<void>;
15
26
  export type WriteRecord = {
16
27
  operation: string;
17
28
  /** What was written, as the user would read it back. */
@@ -6,6 +6,7 @@
6
6
  * and recurring series — are both cases where the obvious code returns
7
7
  * something plausible and wrong, so they moved across unchanged.
8
8
  */
9
+ import { type RecordWrite } from './audit.js';
9
10
  type CalendarTime = {
10
11
  dateTime?: string;
11
12
  date?: string;
@@ -227,7 +228,7 @@ export declare function previewEvent(token: string, params: Record<string, unkno
227
228
  * inexpressible too. It does not prove a person read the preview, and no part
228
229
  * of this protocol can.
229
230
  */
230
- export declare function createEvent(token: string, params: Record<string, unknown>): Promise<{
231
+ export declare function createEvent(token: string, params: Record<string, unknown>, record?: RecordWrite): Promise<{
231
232
  created: {
232
233
  id: string;
233
234
  summary: string;
@@ -259,7 +260,7 @@ export declare function previewDeleteEvent(token: string, params: Record<string,
259
260
  * notices absence — so what is written down has to be enough to put it back by
260
261
  * hand once Google's 30-day bin has expired.
261
262
  */
262
- export declare function deleteEvent(token: string, params: Record<string, unknown>): Promise<{
263
+ export declare function deleteEvent(token: string, params: Record<string, unknown>, record?: RecordWrite): Promise<{
263
264
  deleted: string;
264
265
  calendar_id: string;
265
266
  event_id: string;
package/dist/calendar.js CHANGED
@@ -470,7 +470,7 @@ const describeDuplicate = async (token, calendarId, eventId) => {
470
470
  * inexpressible too. It does not prove a person read the preview, and no part
471
471
  * of this protocol can.
472
472
  */
473
- export async function createEvent(token, params) {
473
+ export async function createEvent(token, params, record = recordWrite) {
474
474
  const draft = draftFrom(params);
475
475
  refuseUnsettled(draft);
476
476
  const supplied = typeof params.confirmation_token === 'string' ? params.confirmation_token : '';
@@ -514,7 +514,7 @@ export async function createEvent(token, params) {
514
514
  // the write another way leaves no trace at all — which is exactly what
515
515
  // happened the first time this was run against a real calendar. The audit has
516
516
  // to sit where the event is created or it is not an audit.
517
- await recordWrite({
517
+ await record({
518
518
  operation: 'create_calendar_event',
519
519
  summary: renderDraft(draft),
520
520
  target: `${draft.calendar_id}/${created.id ?? 'unknown'}`,
@@ -631,7 +631,7 @@ export async function previewDeleteEvent(token, params) {
631
631
  * notices absence — so what is written down has to be enough to put it back by
632
632
  * hand once Google's 30-day bin has expired.
633
633
  */
634
- export async function deleteEvent(token, params) {
634
+ export async function deleteEvent(token, params, record = recordWrite) {
635
635
  const calendarId = typeof params.calendar_id === 'string' && params.calendar_id.trim()
636
636
  ? params.calendar_id.trim()
637
637
  : 'primary';
@@ -646,13 +646,13 @@ export async function deleteEvent(token, params) {
646
646
  : 'The confirmation_token does not match this event. It has changed since ' +
647
647
  'the preview, so preview again and show the musician what is there now.');
648
648
  }
649
- const record = renderExisting(event, calendarId);
649
+ const written = renderExisting(event, calendarId);
650
650
  await calendarDeleteEvent(calendarId, eventId, token);
651
- await recordWrite({
651
+ await record({
652
652
  operation: 'delete_calendar_event',
653
- summary: record,
653
+ summary: written,
654
654
  target: `${calendarId}/${eventId}`,
655
655
  source_page: typeof params.source_page === 'string' ? params.source_page : null,
656
656
  });
657
- return { deleted: record, calendar_id: calendarId, event_id: eventId };
657
+ return { deleted: written, calendar_id: calendarId, event_id: eventId };
658
658
  }
@@ -10,6 +10,7 @@
10
10
  * Google connection has to be reported as such rather than surfacing as an
11
11
  * unrelated Microsoft error.
12
12
  */
13
+ import { type RecordWrite } from './audit.js';
13
14
  import { type ProviderName } from './tokens.js';
14
15
  /**
15
16
  * Every operation this server can carry out, with its provider and its effect.
@@ -109,6 +110,6 @@ export declare const connectedProviders: () => Promise<ProviderName[]>;
109
110
  * stated once regardless of where the token was found.
110
111
  */
111
112
  export type ResolveToken = (provider: ProviderName) => Promise<string>;
112
- export declare const dispatchWith: (resolve: ResolveToken) => <T>(op: Operation, params?: Record<string, unknown>) => Promise<T>;
113
+ export declare const dispatchWith: (resolve: ResolveToken, record?: RecordWrite) => <T>(op: Operation, params?: Record<string, unknown>) => Promise<T>;
113
114
  /** The local dispatcher: this machine's own connections, one process, one user. */
114
115
  export declare const call: <T>(op: Operation, params?: Record<string, unknown>) => Promise<T>;
package/dist/dispatch.js CHANGED
@@ -11,6 +11,7 @@
11
11
  * unrelated Microsoft error.
12
12
  */
13
13
  import { GraphError } from './client.js';
14
+ import { recordWrite } from './audit.js';
14
15
  import { mapAttachment, readAttachment } from './attachments.js';
15
16
  import { createEvent, deleteEvent, listCalendars, listEvents, previewDeleteEvent, previewEvent, readEvent, } from './calendar.js';
16
17
  import { listEmails, readEmail } from './mail.js';
@@ -72,7 +73,7 @@ export const connectedProviders = async () => {
72
73
  const { providers } = await loadTokens();
73
74
  return Object.keys(providers).filter((name) => providers[name] !== undefined);
74
75
  };
75
- export const dispatchWith = (resolve) => async (op, params = {}) => {
76
+ export const dispatchWith = (resolve, record = recordWrite) => async (op, params = {}) => {
76
77
  const token = await resolve(PROVIDER_FOR[op]);
77
78
  switch (op) {
78
79
  case 'list_notes':
@@ -98,11 +99,11 @@ export const dispatchWith = (resolve) => async (op, params = {}) => {
98
99
  case 'preview_calendar_event':
99
100
  return (await previewEvent(token, params));
100
101
  case 'create_calendar_event':
101
- return (await createEvent(token, params));
102
+ return (await createEvent(token, params, record));
102
103
  case 'preview_calendar_delete':
103
104
  return (await previewDeleteEvent(token, params));
104
105
  case 'delete_calendar_event':
105
- return (await deleteEvent(token, params));
106
+ return (await deleteEvent(token, params, record));
106
107
  case 'list_calendars':
107
108
  // No parameters by design: the whole value is knowing the full set, and
108
109
  // a filtered list of what exists is the reach problem again.
package/dist/grants.d.ts CHANGED
@@ -43,21 +43,25 @@ export declare const isWriteCapability: (value: unknown) => value is WriteCapabi
43
43
  * the tool they wanted was missing.
44
44
  */
45
45
  export declare const parseGrants: (raw: string | undefined) => WriteCapability[];
46
- export declare const setGrants: (capabilities: readonly WriteCapability[]) => void;
47
- export declare const grantedWrites: () => readonly WriteCapability[];
48
46
  /**
49
- * Whether a write tool may be registered at all.
47
+ * Whether a write tool may be registered, given what this caller was granted.
50
48
  *
51
- * Registration, not refusal at call time. A tool that exists is a tool a model
49
+ * A parameter, not module state. One stdio process serves one user and could
50
+ * safely hold it in a variable; the hosted route serves many users from one
51
+ * process, where a grant set per request would let one user's capability reach
52
+ * another's session. That failure would pass every test here, because every
53
+ * test runs one user at a time — so the shape has to prevent it rather than the
54
+ * discipline. See docs/decisions/0001-opt-in-calendar-writes.md.
55
+ *
56
+ * Registration, not refusal at call time: a tool that exists is a tool a model
52
57
  * will try, and a refusal inside a tool result reads as an obstacle to route
53
- * around rather than as a boundary — so an ungranted write tool is absent, not
54
- * present and saying no.
58
+ * around rather than as a boundary.
55
59
  */
56
- export declare const isGranted: (capability: WriteCapability) => boolean;
60
+ export declare const isGranted: (grants: readonly WriteCapability[], capability: WriteCapability) => boolean;
57
61
  /**
58
62
  * How the grant is described wherever it is reported: `init`, `agents status`,
59
63
  * and the workflow briefing. A capability nobody can see is one nobody accounts
60
64
  * for, which is the handshake bug again — a correct thing that never reached
61
65
  * the client.
62
66
  */
63
- export declare const describeGrants: () => string;
67
+ export declare const describeGrants: (grants: readonly WriteCapability[]) => string;
package/dist/grants.js CHANGED
@@ -68,36 +68,30 @@ export const parseGrants = (raw) => {
68
68
  return [...new Set(named)];
69
69
  };
70
70
  /**
71
- * What this process may do, decided once at startup.
71
+ * Whether a write tool may be registered, given what this caller was granted.
72
72
  *
73
- * Module state for the same reason the playbook root is: the server is spawned
74
- * by Claude Desktop with whatever `init` wrote, and nothing downstream should
75
- * be re-deriving the answer or able to widen it later.
76
- */
77
- let granted = [];
78
- export const setGrants = (capabilities) => {
79
- granted = [...capabilities];
80
- };
81
- export const grantedWrites = () => granted;
82
- /**
83
- * Whether a write tool may be registered at all.
73
+ * A parameter, not module state. One stdio process serves one user and could
74
+ * safely hold it in a variable; the hosted route serves many users from one
75
+ * process, where a grant set per request would let one user's capability reach
76
+ * another's session. That failure would pass every test here, because every
77
+ * test runs one user at a time — so the shape has to prevent it rather than the
78
+ * discipline. See docs/decisions/0001-opt-in-calendar-writes.md.
84
79
  *
85
- * Registration, not refusal at call time. A tool that exists is a tool a model
80
+ * Registration, not refusal at call time: a tool that exists is a tool a model
86
81
  * will try, and a refusal inside a tool result reads as an obstacle to route
87
- * around rather than as a boundary — so an ungranted write tool is absent, not
88
- * present and saying no.
82
+ * around rather than as a boundary.
89
83
  */
90
- export const isGranted = (capability) => granted.includes(capability);
84
+ export const isGranted = (grants, capability) => grants.includes(capability);
91
85
  /**
92
86
  * How the grant is described wherever it is reported: `init`, `agents status`,
93
87
  * and the workflow briefing. A capability nobody can see is one nobody accounts
94
88
  * for, which is the handshake bug again — a correct thing that never reached
95
89
  * the client.
96
90
  */
97
- export const describeGrants = () => {
98
- if (granted.length === 0) {
91
+ export const describeGrants = (grants) => {
92
+ if (grants.length === 0) {
99
93
  return 'Writes: none. This install can only read.';
100
94
  }
101
- const lines = granted.map((name) => ` - ${name}: ${WRITE_CAPABILITIES[name]}`);
95
+ const lines = grants.map((name) => ` - ${name}: ${WRITE_CAPABILITIES[name]}`);
102
96
  return `Writes granted to this install:\n${lines.join('\n')}`;
103
97
  };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { runInit, runUninstall } from './init.js';
3
3
  import { runServer } from './server.js';
4
4
  import { installAgentPack, runAgentsStatus, setLocalAgentRoot } from './agents.js';
5
5
  import { runConnect, runDisconnect, runStatus } from './connect.js';
6
- import { isWriteCapability, parseGrants, setGrants } from './grants.js';
6
+ import { isWriteCapability, parseGrants } from './grants.js';
7
7
  const argv = process.argv.slice(2);
8
8
  /**
9
9
  * Pull an option and its value out of the arguments wherever they appear.
@@ -67,10 +67,9 @@ try {
67
67
  // only way the server learns where a user's playbooks live: there is no
68
68
  // cwd here worth trusting.
69
69
  setLocalAgentRoot(agentsDir || undefined);
70
- // Decided once, before any tool is registered. An ungranted write tool is
71
- // never built, rather than built and refusing.
72
- setGrants(grants);
73
- await runServer();
70
+ // Handed in, not stashed. An ungranted write tool is never built, rather
71
+ // than built and refusing.
72
+ await runServer(grants);
74
73
  break;
75
74
  case 'init':
76
75
  if (argv[1] && argv[1] !== '--local') {
package/dist/init.js CHANGED
@@ -5,7 +5,7 @@ import { isStagingVersion, packageVersion } from "./client.js";
5
5
  import { sep } from "node:path";
6
6
  import { configPath, ENTRY_NAME, readConfig, writeConfig } from "./config.js";
7
7
  import { connectedProviders } from "./dispatch.js";
8
- import { describeGrants, setGrants } from "./grants.js";
8
+ import { describeGrants } from "./grants.js";
9
9
  const PACKAGE = "@manudota/artist-mcp";
10
10
  const LOCAL_ENTRY = fileURLToPath(new URL("./index.js", import.meta.url));
11
11
  /**
@@ -84,8 +84,7 @@ export const runInit = async ({ local = false, editableDir, grants = [], } = {})
84
84
  // accounts for, and the useful line is the one that appears when the answer
85
85
  // is "none" — that is what tells a user their install is still read-only.
86
86
  console.log("");
87
- setGrants(grants);
88
- console.log(describeGrants());
87
+ console.log(describeGrants(grants));
89
88
  if (grants.length > 0) {
90
89
  console.log("Reconnect Google so the grant is on the token: `artist-mcp connect google`. " +
91
90
  "A refresh token carries the scopes it was granted with, so an existing " +
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { type WriteCapability } from "./grants.js";
2
3
  import { type ResolvedEntry } from "./agents.js";
3
4
  import { type Operation } from "./dispatch.js";
4
5
  /**
@@ -18,11 +19,11 @@ type Dispatch = <T>(op: Operation, params?: Record<string, unknown>) => Promise<
18
19
  */
19
20
  declare const renderWorkflowBriefing: (entries: ResolvedEntry[], load: (id: string) => Promise<{
20
21
  content: string;
21
- }>) => Promise<string>;
22
- declare const createServer: (call: Dispatch) => Promise<McpServer>;
22
+ }>, writes?: readonly WriteCapability[]) => Promise<string>;
23
+ declare const createServer: (call: Dispatch, grants?: readonly WriteCapability[]) => Promise<McpServer>;
23
24
  /**
24
25
  * The stdio entry point, unchanged in behaviour: one process, one user, tokens
25
26
  * from this machine. It is now the only place that knows about stdio.
26
27
  */
27
- declare const runServer: () => Promise<void>;
28
+ declare const runServer: (grants?: readonly WriteCapability[]) => Promise<void>;
28
29
  export { createServer, renderWorkflowBriefing, runServer, type Dispatch };
package/dist/server.js CHANGED
@@ -2,7 +2,7 @@ import { join } from "node:path";
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { z } from "zod";
5
- import { WRITE_CAPABILITIES, grantedWrites, isGranted } from "./grants.js";
5
+ 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";
@@ -103,7 +103,7 @@ const selectNotebook = async (call, notebook, tool) => {
103
103
  : null;
104
104
  return { pages, scope };
105
105
  };
106
- const serverVersion = '1.5.2'; // x-release-please-version
106
+ const serverVersion = '1.5.3-staging.76'; // x-release-please-version
107
107
  const errorResult = (err) => {
108
108
  const message = err instanceof GraphError ? err.message : `Unexpected error: ${err}`;
109
109
  return { content: [{ type: "text", text: message }], isError: true };
@@ -115,7 +115,7 @@ const errorResult = (err) => {
115
115
  * tampering with the installed pack, which is how it was found in the first
116
116
  * place.
117
117
  */
118
- const renderWorkflowBriefing = async (entries, load) => {
118
+ const renderWorkflowBriefing = async (entries, load, writes = []) => {
119
119
  // A one-line summary is enough to pick a role to load, but not to
120
120
  // classify a page: the rules that separate one project type from
121
121
  // another live in the body of the file. Intake is here for the same
@@ -214,7 +214,6 @@ const renderWorkflowBriefing = async (entries, load) => {
214
214
  // seven roles and six policies, so a session must be told plainly when it is
215
215
  // no longer true. The "none" line matters most: it is what keeps every
216
216
  // read-only install saying the same thing it always said.
217
- const writes = grantedWrites();
218
217
  const capabilities = writes.length === 0
219
218
  ? [
220
219
  "# What this install may change",
@@ -251,8 +250,7 @@ const renderWorkflowBriefing = async (entries, load) => {
251
250
  * Derived, never restated. An install with no grant says so too — that line is
252
251
  * the one that keeps a read-only install describing itself correctly.
253
252
  */
254
- const capabilityLine = () => {
255
- const writes = grantedWrites();
253
+ const capabilityLine = (writes) => {
256
254
  if (writes.length === 0) {
257
255
  return (" This install can only read. It cannot create, change or delete " +
258
256
  "anything in OneNote, Gmail or Google Calendar, and must never offer to.");
@@ -263,7 +261,7 @@ const capabilityLine = () => {
263
261
  "this server as read-only while any write is granted, and never write a " +
264
262
  "value the notebook has not settled.");
265
263
  };
266
- const createServer = async (call) => {
264
+ const createServer = async (call, grants = []) => {
267
265
  // Nothing is checked here on purpose. The server starts whether or not a
268
266
  // provider is connected, and a tool that needs one says so when it is called:
269
267
  // refusing to start would leave Claude Desktop reporting a broken server
@@ -302,7 +300,7 @@ const createServer = async (call) => {
302
300
  // This is not the third copy of the rules that drifted in AGENTS.md: it is
303
301
  // derived from the grant at startup, so it cannot say something the install
304
302
  // is not. The rules themselves are still only in the pack.
305
- capabilityLine();
303
+ capabilityLine(grants);
306
304
  const server = new McpServer({ name: "artist-notes", version: serverVersion }, { instructions });
307
305
  server.tool("list_agent_workflows", "List the read-only artist roles, project types, and policies available " +
308
306
  "at runtime. Whatever comes back in full is in force as returned and is " +
@@ -312,7 +310,7 @@ const createServer = async (call) => {
312
310
  "Anything listed as a one-line summary is not in force until loaded. " +
313
311
  "Load the Orchestrator before handling a project.", {}, async () => {
314
312
  try {
315
- const text = await renderWorkflowBriefing(await listAgentWorkflows(), loadAgentWorkflow);
313
+ const text = await renderWorkflowBriefing(await listAgentWorkflows(), loadAgentWorkflow, grants);
316
314
  return { content: [{ type: "text", text }] };
317
315
  }
318
316
  catch (err) {
@@ -793,7 +791,7 @@ const createServer = async (call) => {
793
791
  // present and refusing: a tool that exists is a tool a model will try, and a
794
792
  // refusal in a tool result reads as an obstacle to route around rather than
795
793
  // as a boundary. See docs/decisions/0001-opt-in-calendar-writes.md.
796
- if (isGranted("calendar-create")) {
794
+ if (isGranted(grants, "calendar-create")) {
797
795
  server.tool("preview_calendar_event", "Call this before create_calendar_event — it is the only way to obtain the confirmation_token that one requires, and it is what puts the exact event in front of the musician. SHOW THE MUSICIAN WHAT IT RETURNS AND WAIT FOR THEIR YES. " +
798
796
  "Renders a Google Calendar event exactly as it would be written, and " +
799
797
  "lists what is already on that day in that calendar, so an event that " +
@@ -899,7 +897,7 @@ const createServer = async (call) => {
899
897
  }
900
898
  });
901
899
  }
902
- if (isGranted("calendar-delete")) {
900
+ if (isGranted(grants, "calendar-delete")) {
903
901
  server.tool("preview_calendar_delete", "Call this before delete_calendar_event — it is the only way to obtain the confirmation_token that one requires. SHOW THE MUSICIAN WHAT IT RETURNS AND WAIT FOR THEIR YES. " +
904
902
  "Shows the event that would be removed, read fresh from Google rather " +
905
903
  "than from anything you were told about it. Changes nothing. Only an " +
@@ -1104,8 +1102,8 @@ const createServer = async (call) => {
1104
1102
  * The stdio entry point, unchanged in behaviour: one process, one user, tokens
1105
1103
  * from this machine. It is now the only place that knows about stdio.
1106
1104
  */
1107
- const runServer = async () => {
1108
- const server = await createServer(localCall);
1105
+ const runServer = async (grants = []) => {
1106
+ const server = await createServer(localCall, grants);
1109
1107
  await server.connect(new StdioServerTransport());
1110
1108
  };
1111
1109
  export { createServer, renderWorkflowBriefing, runServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manudota/artist-mcp",
3
- "version": "1.5.2",
3
+ "version": "1.5.3-staging.76",
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",
@@ -14,6 +14,8 @@
14
14
  ".": "./dist/index.js",
15
15
  "./server": "./dist/server.js",
16
16
  "./dispatch": "./dist/dispatch.js",
17
+ "./grants": "./dist/grants.js",
18
+ "./audit": "./dist/audit.js",
17
19
  "./oauth": "./dist/oauth.js",
18
20
  "./client": "./dist/client.js"
19
21
  },