@softeria/ms-365-mcp-server 0.131.1 → 0.131.3

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.
@@ -93,6 +93,15 @@ function createMockServer() {
93
93
  tools.set(name, { description, schema, handler });
94
94
  }
95
95
  ),
96
+ registerTool: vi.fn(
97
+ (name, config, handler) => {
98
+ tools.set(name, {
99
+ description: config.description,
100
+ schema: config.inputSchema?.shape ?? config.inputSchema,
101
+ handler
102
+ });
103
+ }
104
+ ),
96
105
  tools
97
106
  };
98
107
  }
@@ -1100,14 +1100,16 @@
1100
1100
  "method": "post",
1101
1101
  "toolName": "create-todo-task",
1102
1102
  "presets": ["personal", "tasks"],
1103
- "scopes": ["Tasks.ReadWrite"]
1103
+ "scopes": ["Tasks.ReadWrite"],
1104
+ "llmTip": "Creates a new task in a Microsoft To Do list. Body: { title: \"...\" }; optional dueDateTime, reminderDateTime, importance, body (notes), recurrence, categories. Requires todoTaskListId from list-todo-task-lists."
1104
1105
  },
1105
1106
  {
1106
1107
  "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",
1107
1108
  "method": "patch",
1108
1109
  "toolName": "update-todo-task",
1109
1110
  "presets": ["personal", "tasks"],
1110
- "scopes": ["Tasks.ReadWrite"]
1111
+ "scopes": ["Tasks.ReadWrite"],
1112
+ "llmTip": "Updates a Microsoft To Do task. Use this to mark a to-do item complete or done (body: { status: \"completed\" }), reopen it (status: \"notStarted\"), rename it (title), or change its due date (dueDateTime), reminder (reminderDateTime), importance, or notes (body). Requires todoTaskListId from list-todo-task-lists and todoTaskId from list-todo-tasks."
1111
1113
  },
1112
1114
  {
1113
1115
  "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",
@@ -395,6 +395,44 @@ function registerUtilityToolWithMcp(server, utility, ctx) {
395
395
  async (params) => utility.execute(params, ctx)
396
396
  );
397
397
  }
398
+ function bodySchemaShape(schema) {
399
+ let current = schema;
400
+ for (let i = 0; i < 10 && current; i++) {
401
+ if (current instanceof z.ZodObject) {
402
+ return current.shape;
403
+ }
404
+ const def = current._def;
405
+ current = def?.innerType ?? def?.schema ?? def?.getter?.();
406
+ }
407
+ return null;
408
+ }
409
+ function lenientBodySchema(schema) {
410
+ if (schema instanceof z.ZodObject) {
411
+ return schema.passthrough();
412
+ }
413
+ if (schema instanceof z.ZodOptional) {
414
+ return lenientBodySchema(schema.unwrap()).optional();
415
+ }
416
+ if (schema instanceof z.ZodNullable) {
417
+ return lenientBodySchema(schema.unwrap()).nullable();
418
+ }
419
+ if (schema instanceof z.ZodLazy) {
420
+ return z.lazy(() => lenientBodySchema(schema.schema));
421
+ }
422
+ return schema;
423
+ }
424
+ const READ_ONLY_BODY_FIELDS = /* @__PURE__ */ new Set([
425
+ "id",
426
+ "createdDateTime",
427
+ "lastModifiedDateTime",
428
+ "changeKey"
429
+ ]);
430
+ function isPlainObject(value) {
431
+ return typeof value === "object" && value !== null && !Array.isArray(value);
432
+ }
433
+ function hasOwn(obj, key) {
434
+ return Object.prototype.hasOwnProperty.call(obj, key);
435
+ }
398
436
  async function executeGraphTool(tool, config, graphClient, params, authManager) {
399
437
  logger.info(`Tool ${tool.alias} called with params: ${JSON.stringify(params)}`);
400
438
  if (isConfirmGateEnabled() && isDestructiveOperation(tool.method, config) && params.confirm !== true) {
@@ -451,6 +489,10 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
451
489
  const queryParams = {};
452
490
  const headers = {};
453
491
  let body = null;
492
+ const bodyShape = bodySchemaShape(
493
+ parameterDefinitions.find((p) => p.type === "Body")?.schema
494
+ );
495
+ const strayBodyFields = {};
454
496
  for (const [paramName, paramValue] of Object.entries(params)) {
455
497
  if ([
456
498
  "account",
@@ -529,6 +571,29 @@ async function executeGraphTool(tool, config, graphClient, params, authManager)
529
571
  } else if (isOdataParam) {
530
572
  queryParams[fixedParamName] = `${paramValue}`;
531
573
  logger.info(`OData param fallback: forwarded ${fixedParamName}=${paramValue}`);
574
+ } else if (bodyShape && (hasOwn(bodyShape, paramName) || hasOwn(bodyShape, camelCaseParamName)) && !READ_ONLY_BODY_FIELDS.has(hasOwn(bodyShape, paramName) ? paramName : camelCaseParamName)) {
575
+ const fieldName = hasOwn(bodyShape, paramName) ? paramName : camelCaseParamName;
576
+ strayBodyFields[fieldName] = paramValue;
577
+ logger.info(
578
+ `Body field fallback: merging top-level param '${fieldName}' into request body`
579
+ );
580
+ } else {
581
+ logger.warn(`Dropping unrecognized parameter '${paramName}' for tool ${tool.alias}`);
582
+ }
583
+ }
584
+ if (Object.keys(strayBodyFields).length > 0) {
585
+ if (isPlainObject(body)) {
586
+ const keys = Object.keys(body);
587
+ const bodyIsNestedField = bodyShape != null && hasOwn(bodyShape, "body") && keys.length > 0 && keys.every((k) => !hasOwn(bodyShape, k));
588
+ body = bodyIsNestedField ? { ...strayBodyFields, body } : { ...strayBodyFields, ...body };
589
+ logger.info(`Merged flattened body fields: ${Object.keys(strayBodyFields).join(", ")}`);
590
+ } else if (body == null) {
591
+ body = strayBodyFields;
592
+ logger.info(`Merged flattened body fields: ${Object.keys(strayBodyFields).join(", ")}`);
593
+ } else {
594
+ logger.warn(
595
+ `Cannot merge flattened body fields (${Object.keys(strayBodyFields).join(", ")}) into non-object request body; dropping them`
596
+ );
532
597
  }
533
598
  }
534
599
  if (TOP_UNSUPPORTED_DELTA_TOOLS.has(tool.alias)) {
@@ -792,7 +857,7 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
792
857
  const paramSchema = {};
793
858
  if (tool.parameters && tool.parameters.length > 0) {
794
859
  for (const param of tool.parameters) {
795
- paramSchema[param.name] = param.schema || z.any();
860
+ paramSchema[param.name] = param.type === "Body" && param.schema ? lenientBodySchema(param.schema) : param.schema || z.any();
796
861
  }
797
862
  }
798
863
  const pathParamMatches = tool.path.matchAll(/:([a-zA-Z]+)/g);
@@ -880,16 +945,19 @@ function registerGraphTools(server, graphClient, readOnly = false, enabledToolsP
880
945
  }
881
946
  const isReadOnlyTool = tool.method.toUpperCase() === "GET" || endpointConfig?.readOnly === true;
882
947
  try {
883
- server.tool(
948
+ server.registerTool(
884
949
  tool.alias,
885
- toolDescription,
886
- paramSchema,
887
950
  {
888
951
  title: tool.alias,
889
- readOnlyHint: isReadOnlyTool,
890
- destructiveHint: destructive,
891
- openWorldHint: true
892
- // All tools call Microsoft Graph API
952
+ description: toolDescription,
953
+ inputSchema: z.object(paramSchema).passthrough(),
954
+ annotations: {
955
+ title: tool.alias,
956
+ readOnlyHint: isReadOnlyTool,
957
+ destructiveHint: destructive,
958
+ openWorldHint: true
959
+ // All tools call Microsoft Graph API
960
+ }
893
961
  },
894
962
  async (params) => executeGraphTool(tool, endpointConfig, graphClient, params, authManager)
895
963
  );
@@ -0,0 +1,112 @@
1
+ import { ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
2
+ import logger from "./logger.js";
3
+ function unescapePointer(token) {
4
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
5
+ }
6
+ function isBadRef(value) {
7
+ return typeof value === "string" && value.startsWith("#/") && !value.startsWith("#/$defs/");
8
+ }
9
+ function collectBadRefTargets(node, targets) {
10
+ if (!node || typeof node !== "object") return;
11
+ if (Array.isArray(node)) {
12
+ for (const item of node) collectBadRefTargets(item, targets);
13
+ return;
14
+ }
15
+ for (const [key, value] of Object.entries(node)) {
16
+ if (key === "$ref" && isBadRef(value)) {
17
+ targets.add(value.slice(1));
18
+ } else {
19
+ collectBadRefTargets(value, targets);
20
+ }
21
+ }
22
+ }
23
+ function resolvePointer(root, pointer) {
24
+ const tokens = pointer.split("/").slice(1).map(unescapePointer);
25
+ if (tokens.length === 0) return null;
26
+ let parent = null;
27
+ let key = "";
28
+ let cur = root;
29
+ for (const token of tokens) {
30
+ if (!cur || typeof cur !== "object") return null;
31
+ parent = cur;
32
+ key = Array.isArray(cur) ? Number(token) : token;
33
+ cur = cur[key];
34
+ if (cur === void 0) return null;
35
+ }
36
+ return parent === null ? null : { parent, key, obj: cur };
37
+ }
38
+ function repointRefs(node, nameFor) {
39
+ if (!node || typeof node !== "object") return;
40
+ if (Array.isArray(node)) {
41
+ for (const item of node) repointRefs(item, nameFor);
42
+ return;
43
+ }
44
+ const record = node;
45
+ for (const [key, value] of Object.entries(record)) {
46
+ if (key === "$ref" && isBadRef(value)) {
47
+ const name = nameFor.get(value.slice(1));
48
+ if (name) record.$ref = `#/$defs/${name}`;
49
+ } else {
50
+ repointRefs(value, nameFor);
51
+ }
52
+ }
53
+ }
54
+ function normalizeToolSchemaRefs(schema) {
55
+ const targets = /* @__PURE__ */ new Set();
56
+ collectBadRefTargets(schema, targets);
57
+ if (targets.size === 0) return schema;
58
+ const clone = structuredClone(schema);
59
+ const slots = /* @__PURE__ */ new Map();
60
+ for (const pointer of [...targets].sort()) {
61
+ const slot = resolvePointer(clone, pointer);
62
+ if (slot) slots.set(pointer, slot);
63
+ }
64
+ if (slots.size === 0) return schema;
65
+ const takenNames = new Set(
66
+ clone.$defs && typeof clone.$defs === "object" ? Object.keys(clone.$defs) : []
67
+ );
68
+ const nameFor = /* @__PURE__ */ new Map();
69
+ let index = 0;
70
+ for (const pointer of slots.keys()) {
71
+ let name = `def${index++}`;
72
+ while (takenNames.has(name)) name = `def${index++}`;
73
+ takenNames.add(name);
74
+ nameFor.set(pointer, name);
75
+ }
76
+ const defs = {};
77
+ for (const [pointer, slot] of slots) {
78
+ defs[nameFor.get(pointer)] = slot.obj;
79
+ }
80
+ for (const [pointer, slot] of slots) {
81
+ slot.parent[slot.key] = {
82
+ $ref: `#/$defs/${nameFor.get(pointer)}`
83
+ };
84
+ }
85
+ repointRefs(clone, nameFor);
86
+ for (const name of Object.keys(defs)) repointRefs(defs[name], nameFor);
87
+ const existingDefs = clone.$defs ?? {};
88
+ clone.$defs = { ...existingDefs, ...defs };
89
+ return clone;
90
+ }
91
+ function installToolSchemaRefNormalization(server) {
92
+ const lowLevel = server.server;
93
+ const handlers = lowLevel._requestHandlers;
94
+ const original = handlers?.get("tools/list");
95
+ if (!original) {
96
+ logger.warn("Skipping tool-schema $ref normalization: tools/list handler not found");
97
+ return;
98
+ }
99
+ lowLevel.setRequestHandler(ListToolsRequestSchema, async (request, extra) => {
100
+ const result = await original(request, extra);
101
+ for (const tool of result.tools ?? []) {
102
+ if (tool.inputSchema && typeof tool.inputSchema === "object") {
103
+ tool.inputSchema = normalizeToolSchemaRefs(tool.inputSchema);
104
+ }
105
+ }
106
+ return result;
107
+ });
108
+ }
109
+ export {
110
+ installToolSchemaRefNormalization,
111
+ normalizeToolSchemaRefs
112
+ };
package/dist/server.js CHANGED
@@ -9,6 +9,7 @@ import logger, { enableConsoleLogging } from "./logger.js";
9
9
  import { registerAuthTools } from "./auth-tools.js";
10
10
  import { registerGraphTools, registerDiscoveryTools } from "./graph-tools.js";
11
11
  import { buildMcpServerInstructions } from "./mcp-instructions.js";
12
+ import { installToolSchemaRefNormalization } from "./normalize-tool-schema.js";
12
13
  import GraphClient from "./graph-client.js";
13
14
  import {
14
15
  buildScopesFromEndpoints,
@@ -102,6 +103,7 @@ class MicrosoftGraphServer {
102
103
  this.options.allowedScopes
103
104
  );
104
105
  }
106
+ installToolSchemaRefNormalization(server);
105
107
  return server;
106
108
  }
107
109
  async initialize(version) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softeria/ms-365-mcp-server",
3
- "version": "0.131.1",
3
+ "version": "0.131.3",
4
4
  "description": " A Model Context Protocol (MCP) server for interacting with Microsoft 365 and Office services through the Graph API",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1100,14 +1100,16 @@
1100
1100
  "method": "post",
1101
1101
  "toolName": "create-todo-task",
1102
1102
  "presets": ["personal", "tasks"],
1103
- "scopes": ["Tasks.ReadWrite"]
1103
+ "scopes": ["Tasks.ReadWrite"],
1104
+ "llmTip": "Creates a new task in a Microsoft To Do list. Body: { title: \"...\" }; optional dueDateTime, reminderDateTime, importance, body (notes), recurrence, categories. Requires todoTaskListId from list-todo-task-lists."
1104
1105
  },
1105
1106
  {
1106
1107
  "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",
1107
1108
  "method": "patch",
1108
1109
  "toolName": "update-todo-task",
1109
1110
  "presets": ["personal", "tasks"],
1110
- "scopes": ["Tasks.ReadWrite"]
1111
+ "scopes": ["Tasks.ReadWrite"],
1112
+ "llmTip": "Updates a Microsoft To Do task. Use this to mark a to-do item complete or done (body: { status: \"completed\" }), reopen it (status: \"notStarted\"), rename it (title), or change its due date (dueDateTime), reminder (reminderDateTime), importance, or notes (body). Requires todoTaskListId from list-todo-task-lists and todoTaskId from list-todo-tasks."
1111
1113
  },
1112
1114
  {
1113
1115
  "pathPattern": "/me/todo/lists/{todoTaskList-id}/tasks/{todoTask-id}",