add validations against spec, add reserved metadata key validations
This commit is contained in:
parent
3044d8f14c
commit
a45247a4f2
1 changed files with 489 additions and 108 deletions
162
client/src/utils/metaUtils.ts
Normal file
162
client/src/utils/metaUtils.ts
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
/**
|
||||
* Metadata helpers aligned with the official MCP specification.
|
||||
*
|
||||
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta
|
||||
*/
|
||||
|
||||
const META_PREFIX_LABEL_REGEX = /^[a-z](?:[a-z\d-]*[a-z\d])?$/i;
|
||||
const META_NAME_REGEX = /^[a-z\d](?:[a-z\d._-]*[a-z\d])?$/i;
|
||||
const RESERVED_NAMESPACE_LABELS = ["modelcontextprotocol", "mcp"];
|
||||
|
||||
export const RESERVED_NAMESPACE_MESSAGE =
|
||||
'Keys using the "modelcontextprotocol.*" or "mcp.*" namespaces are reserved by MCP and cannot be used.';
|
||||
|
||||
export const META_NAME_RULES_MESSAGE =
|
||||
"Names must begin and end with an alphanumeric character and may only contain alphanumerics, hyphens (-), underscores (_), or dots (.) in between.";
|
||||
|
||||
export const META_PREFIX_RULES_MESSAGE =
|
||||
"Prefixes must be dot-separated labels that start with a letter and end with a letter or digit (e.g. example.domain/).";
|
||||
|
||||
/**
|
||||
* Extracts the prefix portion (before the first slash) of a metadata key, if present.
|
||||
*
|
||||
* @param key - Raw metadata key entered by the user.
|
||||
* @returns The prefix segment (without the trailing slash) or null when no prefix exists.
|
||||
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta
|
||||
*/
|
||||
const getPrefixSegment = (key: string): string | null => {
|
||||
const trimmedKey = key.trim();
|
||||
const slashIndex = trimmedKey.indexOf("/");
|
||||
if (slashIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
return trimmedKey.slice(0, slashIndex);
|
||||
};
|
||||
|
||||
/**
|
||||
* Normalizes a potential prefix segment by trimming whitespace, removing schemes,
|
||||
* and stripping trailing URL components so only the label portion remains.
|
||||
*
|
||||
* @param segment - The prefix segment extracted from the metadata key.
|
||||
* @returns A normalized string suitable for label parsing, or null when empty.
|
||||
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta
|
||||
*/
|
||||
const normalizeSegment = (segment: string): string | null => {
|
||||
if (!segment) return null;
|
||||
let normalized = segment.trim().toLowerCase();
|
||||
if (!normalized) return null;
|
||||
|
||||
const schemeIndex = normalized.indexOf("://");
|
||||
if (schemeIndex !== -1) {
|
||||
normalized = normalized.slice(schemeIndex + 3);
|
||||
}
|
||||
|
||||
const stopChars = ["?", "#", ":"];
|
||||
let endIndex = normalized.length;
|
||||
stopChars.forEach((char) => {
|
||||
const idx = normalized.indexOf(char);
|
||||
if (idx !== -1 && idx < endIndex) {
|
||||
endIndex = idx;
|
||||
}
|
||||
});
|
||||
|
||||
return normalized.slice(0, endIndex) || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits a normalized prefix into dot-separated labels and validates each label
|
||||
* against the MCP prefix rules (start with letter, end with letter/digit, interior alphanumerics or hyphens).
|
||||
*
|
||||
* @param segment - Normalized prefix string.
|
||||
* @returns Array of labels if valid, otherwise null.
|
||||
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta
|
||||
*/
|
||||
const splitLabels = (segment: string): string[] | null => {
|
||||
const normalized = normalizeSegment(segment);
|
||||
if (!normalized) return null;
|
||||
|
||||
const labels = normalized.split(".");
|
||||
if (
|
||||
labels.length === 0 ||
|
||||
labels.some((label) => !label || !META_PREFIX_LABEL_REGEX.test(label))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return labels;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines whether a metadata key is within the MCP-reserved namespace.
|
||||
*
|
||||
* @param key - Full metadata key entered by the user.
|
||||
* @returns True if the key's prefix belongs to a reserved namespace.
|
||||
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta
|
||||
*/
|
||||
export const isReservedMetaKey = (key: string): boolean => {
|
||||
const trimmedKey = key.trim();
|
||||
if (!trimmedKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const candidateSegment = getPrefixSegment(trimmedKey) ?? trimmedKey;
|
||||
const labels = splitLabels(candidateSegment);
|
||||
if (!labels || labels.length < 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let i = 0; i < labels.length - 1; i += 1) {
|
||||
const current = labels[i];
|
||||
const next = labels[i + 1];
|
||||
if (
|
||||
RESERVED_NAMESPACE_LABELS.includes(current) &&
|
||||
META_PREFIX_LABEL_REGEX.test(next)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the optional prefix portion of a metadata key.
|
||||
*
|
||||
* @param key - Full metadata key entered by the user.
|
||||
* @returns True when the prefix is absent or satisfies the MCP label requirements.
|
||||
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta
|
||||
*/
|
||||
export const hasValidMetaPrefix = (key: string): boolean => {
|
||||
const prefixSegment = getPrefixSegment(key);
|
||||
if (prefixSegment === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return splitLabels(prefixSegment) !== null;
|
||||
};
|
||||
|
||||
const extractMetaName = (key: string): string => {
|
||||
const trimmedKey = key.trim();
|
||||
if (!trimmedKey) return "";
|
||||
|
||||
const slashIndex = trimmedKey.lastIndexOf("/");
|
||||
if (slashIndex === -1) {
|
||||
return trimmedKey;
|
||||
}
|
||||
|
||||
return trimmedKey.slice(slashIndex + 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates the "name" portion of a metadata key, regardless of whether a prefix exists.
|
||||
*
|
||||
* @param key - Full metadata key entered by the user.
|
||||
* @returns True if the name portion is valid per the MCP spec.
|
||||
* @see https://modelcontextprotocol.io/specification/2025-06-18/basic/index#meta
|
||||
*/
|
||||
export const hasValidMetaName = (key: string): boolean => {
|
||||
const name = extractMetaName(key);
|
||||
if (!name) return false;
|
||||
|
||||
return META_NAME_REGEX.test(name);
|
||||
};
|
||||
Loading…
Reference in a new issue