notion-mcp-server 2.11.0 → 2.12.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.
@@ -1,4 +1,7 @@
1
1
  import { z } from "zod";
2
+ import { readFile } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { basename } from "node:path";
2
5
  import { getClient } from "../services/notion.js";
3
6
  import { register } from "./registry.js";
4
7
  import { tryHandler } from "../utils/handler.js";
@@ -16,7 +19,23 @@ const SourceSchema = z.discriminatedUnion("type", [
16
19
  type: z.literal("url"),
17
20
  url: z.url().describe("Public URL to fetch the file bytes from."),
18
21
  }),
22
+ z.object({
23
+ type: z.literal("path"),
24
+ path: z
25
+ .string()
26
+ .describe("Local filesystem path (absolute, or ~-relative). The server reads the file directly — bytes never pass through the tool call, so this is the fastest, cheapest source for files on the same machine as the server."),
27
+ }),
19
28
  ]);
29
+ // Expand a leading ~ or ~/ to the current user's home directory. Node's fs
30
+ // does not do this itself, and ~-relative paths are the common shape a caller
31
+ // hands to a local stdio server.
32
+ function expandHome(p) {
33
+ if (p === "~")
34
+ return homedir();
35
+ if (p.startsWith("~/"))
36
+ return homedir() + p.slice(1);
37
+ return p;
38
+ }
20
39
  // Returns Uint8Array<ArrayBuffer> — the DOM Blob constructor's BlobPart type
21
40
  // rejects Uint8Array<ArrayBufferLike> under newer @types/node (it widens to
22
41
  // include SharedArrayBuffer). Allocating fresh guarantees the concrete type.
@@ -27,6 +46,12 @@ async function resolveBytes(source) {
27
46
  out.set(buf);
28
47
  return out;
29
48
  }
49
+ if (source.type === "path") {
50
+ const buf = await readFile(expandHome(source.path));
51
+ const out = new Uint8Array(buf.byteLength);
52
+ out.set(buf);
53
+ return out;
54
+ }
30
55
  const res = await fetch(source.url);
31
56
  if (!res.ok) {
32
57
  throw new Error(`Failed to fetch ${source.url}: ${res.status} ${res.statusText}`);
@@ -97,8 +122,17 @@ const EXTENSION_TO_MIME = {
97
122
  // Documents
98
123
  csv: "text/csv",
99
124
  json: "application/json",
125
+ md: "text/markdown",
126
+ markdown: "text/markdown",
100
127
  pdf: "application/pdf",
101
128
  txt: "text/plain",
129
+ // Microsoft Office
130
+ doc: "application/msword",
131
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
132
+ ppt: "application/vnd.ms-powerpoint",
133
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
134
+ xls: "application/vnd.ms-excel",
135
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
102
136
  };
103
137
  function inferContentType(filename) {
104
138
  const dot = filename.lastIndexOf(".");
@@ -115,7 +149,10 @@ const UploadFileParams = z.object({
115
149
  .enum(["single", "multi"])
116
150
  .optional()
117
151
  .describe("'single' (default) = one create+send call. 'multi' = chunk into 5MB parts then complete."),
118
- filename: z.string(),
152
+ filename: z
153
+ .string()
154
+ .optional()
155
+ .describe("Required for base64 and url sources. Optional for a path source — defaults to the file's basename."),
119
156
  content_type: z.string().optional(),
120
157
  source: SourceSchema,
121
158
  });
@@ -123,7 +160,7 @@ register({
123
160
  name: "upload_file",
124
161
  access: "write",
125
162
  domain: "files",
126
- description: "Upload a file via Notion's file_uploads API. Handles single-part (one create + one send) and multi-part (create + N sends + complete) transparently.\n\nSource shapes:\n • Base64 bytes: `source: { type: \"base64\", data: \"<b64 string>\" }`\n • Public URL: `source: { type: \"url\", url: \"https://example.com/file.pdf\" }` (the server fetches it server-side).\n\n`mode` defaults to \"single\"; only pass \"multi\" for files larger than ~5MB.",
163
+ description: "Upload a file via Notion's file_uploads API. Handles single-part (one create + one send) and multi-part (create + N sends + complete) transparently.\n\nSource shapes:\n • Local path: `source: { type: \"path\", path: \"/abs/or/~/file.pdf\" }` (server reads the file directly — preferred for local files; filename is derived from the path if omitted).\n • Base64 bytes: `source: { type: \"base64\", data: \"<b64 string>\" }`\n • Public URL: `source: { type: \"url\", url: \"https://example.com/file.pdf\" }` (the server fetches it server-side).\n\n`mode` defaults to \"single\"; only pass \"multi\" for files larger than ~5MB.",
127
164
  batchable: false,
128
165
  schema: UploadFileParams,
129
166
  example: {
@@ -133,19 +170,34 @@ register({
133
170
  },
134
171
  handler: tryHandler(async ({ mode, filename, content_type, source }) => {
135
172
  const effectiveMode = mode ?? "single";
173
+ // A path source carries its own name; fall back to the basename when the
174
+ // caller doesn't pass filename explicitly. base64/url have no name to
175
+ // derive, so filename stays required there.
176
+ const effectiveFilename = filename ??
177
+ (source.type === "path" ? basename(expandHome(source.path)) : undefined);
178
+ if (!effectiveFilename) {
179
+ return {
180
+ ok: false,
181
+ error: {
182
+ code: "validation_error",
183
+ message: "filename is required for base64 and url sources (there is no name to derive).",
184
+ fix: 'Pass `filename` (e.g. "report.pdf"), or use a path source to derive it from the path.',
185
+ },
186
+ };
187
+ }
136
188
  const notion = await getClient();
137
189
  const bytes = await resolveBytes(source);
138
190
  // Notion rejects send() when the Blob's MIME doesn't match the
139
191
  // content_type stored at create(), and rejects application/octet-stream
140
192
  // outright. Resolve a single MIME for both sides: caller's content_type
141
193
  // wins, else infer from the filename extension.
142
- const effectiveType = content_type ?? inferContentType(filename);
194
+ const effectiveType = content_type ?? inferContentType(effectiveFilename);
143
195
  if (!effectiveType) {
144
196
  return {
145
197
  ok: false,
146
198
  error: {
147
199
  code: "validation_error",
148
- message: `Could not infer content_type from filename "${filename}". Notion's File Upload API rejects application/octet-stream and only accepts a fixed allowlist of MIME types.`,
200
+ message: `Could not infer content_type from filename "${effectiveFilename}". Notion's File Upload API rejects application/octet-stream and only accepts a fixed allowlist of MIME types.`,
149
201
  fix: "Pass `content_type` explicitly (e.g. \"application/pdf\", \"image/png\", \"text/plain\"). See https://developers.notion.com/docs/working-with-files-and-media for the full list.",
150
202
  },
151
203
  };
@@ -153,13 +205,16 @@ register({
153
205
  if (effectiveMode === "single") {
154
206
  const createBody = {
155
207
  mode: "single_part",
156
- filename,
208
+ filename: effectiveFilename,
157
209
  content_type: effectiveType,
158
210
  };
159
211
  const created = await notion.fileUploads.create(createBody);
160
212
  const sendBody = {
161
213
  file_upload_id: created.id,
162
- file: { filename, data: new Blob([bytes], { type: effectiveType }) },
214
+ file: {
215
+ filename: effectiveFilename,
216
+ data: new Blob([bytes], { type: effectiveType }),
217
+ },
163
218
  };
164
219
  const sent = await notion.fileUploads.send(sendBody);
165
220
  return { ok: true, data: slimFileUpload(sent) };
@@ -167,7 +222,7 @@ register({
167
222
  const parts = splitIntoParts(bytes);
168
223
  const createBody = {
169
224
  mode: "multi_part",
170
- filename,
225
+ filename: effectiveFilename,
171
226
  content_type: effectiveType,
172
227
  number_of_parts: parts.length,
173
228
  };
@@ -176,7 +231,10 @@ register({
176
231
  const partNumber = index + 1;
177
232
  const sendBody = {
178
233
  file_upload_id: created.id,
179
- file: { filename, data: new Blob([part], { type: effectiveType }) },
234
+ file: {
235
+ filename: effectiveFilename,
236
+ data: new Blob([part], { type: effectiveType }),
237
+ },
180
238
  part_number: String(partNumber),
181
239
  };
182
240
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "notion-mcp-server",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "mcpName": "io.github.awkoy/notion-mcp-server",
5
5
  "type": "module",
6
6
  "bin": {