notion-mcp-server
Version:
Notion MCP server with token auth: connect Claude Code, Claude Desktop, Cursor, VS Code, Codex and any MCP client to Notion. Search, create and edit pages, query databases, comment and upload files. Runs headless in CI and agents.
175 lines (174 loc) • 6.45 kB
JavaScript
// Slice a JSON Schema down to the failing field at a given path, and
// summarize any unions / deep nesting so the error envelope stays small.
//
// The full schema is always available via `notion_describe`; in a validation
// error we only need to show the LLM what the failing field expects, plus a
// working example. The unsliced schemas can be 10+KB (set_page_property's
// value union, update_database's properties propertyNames union, etc.).
const DEFAULT_SUMMARY_DEPTH = 2;
function resolveRef(node, defs) {
const ref = node["$ref"];
if (typeof ref !== "string" || !defs)
return node;
const match = /^#\/\$defs\/(.+)$/.exec(ref);
if (!match)
return node;
if (!Object.hasOwn(defs, match[1]))
return node;
const resolved = defs[match[1]];
if (typeof resolved !== "object" || resolved === null)
return node;
return resolved;
}
function asObject(value) {
if (typeof value !== "object" || value === null)
return undefined;
return value;
}
export function sliceJsonSchema(root, path) {
const defs = root.$defs;
let cur = root;
for (const seg of path) {
cur = resolveRef(cur, defs);
if (typeof seg === "string") {
// Only follow own-property keys: a Zod error path comes from caller-supplied
// payload field names, so `__proto__` / `constructor` could otherwise walk
// the prototype chain when slicing.
const props = asObject(cur.properties);
if (props && Object.hasOwn(props, seg)) {
const next = asObject(props[seg]);
if (next) {
cur = next;
continue;
}
}
const branches = (cur.oneOf ?? cur.anyOf);
if (Array.isArray(branches)) {
const matched = branches
.map((b) => resolveRef(b, defs))
.find((b) => {
const p = asObject(b.properties);
return p !== undefined && Object.hasOwn(p, seg);
});
if (matched) {
const props = asObject(matched.properties);
if (props) {
const next = asObject(props[seg]);
if (next) {
cur = next;
continue;
}
}
}
}
// A record schema (z.record) carries its value schema under
// additionalProperties and has no `properties` at all. A page property
// map is one, so a path like ["properties","Tags"] ends here otherwise,
// and the caller gets the whole property-value union back.
const additional = asObject(cur.additionalProperties);
if (additional) {
cur = additional;
continue;
}
return cur;
}
if (typeof seg === "number") {
const items = cur.items;
if (Array.isArray(items)) {
const next = asObject(items[seg]);
if (next) {
cur = next;
continue;
}
}
else if (items) {
const next = asObject(items);
if (next) {
cur = next;
continue;
}
}
return cur;
}
}
return resolveRef(cur, defs);
}
// Replace large unions with a one-line-per-branch discriminator summary,
// and cap nested objects/arrays at DEFAULT_SUMMARY_DEPTH.
export function summarizeSchema(schema, depth = DEFAULT_SUMMARY_DEPTH, defs) {
const allDefs = defs ?? schema.$defs;
const resolved = resolveRef(schema, allDefs);
if (depth < 0) {
const t = resolved.type;
return { _truncated: true, ...(t ? { type: t } : {}) };
}
for (const key of ["oneOf", "anyOf"]) {
const branches = resolved[key];
if (Array.isArray(branches)) {
const summary = branches.map((b) => discriminatorSummary(b, allDefs));
const out = { [key]: summary };
if (resolved.description)
out.description = resolved.description;
return out;
}
}
if (resolved.type === "object" && asObject(resolved.properties)) {
const props = resolved.properties;
const summarizedProps = {};
for (const [k, v] of Object.entries(props)) {
summarizedProps[k] = summarizeSchema(v, depth - 1, allDefs);
}
const out = { ...resolved, properties: summarizedProps };
delete out.$defs;
return out;
}
const additional = asObject(resolved.additionalProperties);
if (additional) {
const out = {
...resolved,
additionalProperties: summarizeSchema(additional, depth - 1, allDefs),
};
delete out.$defs;
return out;
}
if (resolved.type === "array") {
const items = resolved.items;
if (items && !Array.isArray(items)) {
const out = {
...resolved,
items: summarizeSchema(items, depth - 1, allDefs),
};
delete out.$defs;
return out;
}
}
const out = { ...resolved };
delete out.$defs;
return out;
}
// One-line-per-branch: if a branch has a property with a literal `const`
// (Zod literals emit as such), surface it as the discriminator tag. Otherwise
// fall back to `type` + the first required field name.
function discriminatorSummary(branch, defs) {
const resolved = resolveRef(branch, defs);
const props = asObject(resolved.properties);
if (props) {
for (const [k, v] of Object.entries(props)) {
const val = asObject(v);
if (val && val.const !== undefined) {
return { [k]: val.const };
}
}
const required = resolved.required;
if (Array.isArray(required) && required.length > 0) {
return { type: "object", required };
}
}
if (resolved.type)
return { type: resolved.type };
if (resolved.const !== undefined)
return { const: resolved.const };
if (Array.isArray(resolved.enum))
return { enum: resolved.enum };
return resolved;
}