@skilljack/mcp
Advanced tools
@@ -37,3 +37,2 @@ /** | ||
| userInvocable?: boolean; | ||
| metadata?: Record<string, string>; | ||
| effectiveAssistantInvocable: boolean; | ||
@@ -51,19 +50,2 @@ effectiveUserInvocable: boolean; | ||
| /** | ||
| * Validate a metadata key against MCP spec _meta key naming rules. | ||
| * | ||
| * Valid keys have two segments: an optional prefix and a name. | ||
| * - Prefix: labels separated by dots, followed by `/`. Labels start with a letter, | ||
| * end with letter/digit, interior can be letters/digits/hyphens. | ||
| * Implementations SHOULD use reverse DNS notation (e.g., `com.example/`). | ||
| * - Name: starts/ends with alphanumeric, interior can be alphanumerics/hyphens/underscores/dots. | ||
| * May be empty if prefix is present. | ||
| * - Reserved: prefixes where the second label is `modelcontextprotocol` or `mcp` | ||
| * (e.g., `io.modelcontextprotocol/`, `dev.mcp/`). Note: `com.example.mcp/` is NOT reserved. | ||
| */ | ||
| export declare function validateMetaKey(key: string): { | ||
| valid: boolean; | ||
| reserved?: boolean; | ||
| reason?: string; | ||
| }; | ||
| /** | ||
| * Discover all skills in a directory. | ||
@@ -113,3 +95,3 @@ * Scans for subdirectories containing SKILL.md files. | ||
| /** | ||
| * Compute MCP resource annotations for a skill. | ||
| * Compute MCP resource annotations and file size for a skill. | ||
| * Derives audience from effective invocation flags and sets default priority. | ||
@@ -119,7 +101,10 @@ * | ||
| * @param priority - Priority hint (0.0 = least important, 1.0 = most important, default 0.5) | ||
| * @returns Annotations object for use on MCP resources | ||
| * @returns Object with annotations and optional size (in bytes) for use on MCP resources | ||
| */ | ||
| export declare function getResourceAnnotations(skill: SkillMetadata, priority?: number): Annotations; | ||
| export declare function getResourceAnnotations(skill: SkillMetadata, priority?: number): { | ||
| annotations: Annotations; | ||
| size?: number; | ||
| }; | ||
| export declare const SKILL_COUNT_WARNING_THRESHOLD = 50; | ||
| export declare function warnLargeSkillCount(skillCount: number): void; | ||
| export {}; |
@@ -70,56 +70,2 @@ /** | ||
| /** | ||
| * Regex patterns for MCP _meta key validation. | ||
| * See: https://modelcontextprotocol.io/specification/2025-11-25/basic/index#_meta | ||
| */ | ||
| const LABEL_PATTERN = /^[a-zA-Z](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?$/; | ||
| const NAME_PATTERN = /^[a-zA-Z0-9](?:[a-zA-Z0-9._-]*[a-zA-Z0-9])?$/; | ||
| /** | ||
| * Validate a metadata key against MCP spec _meta key naming rules. | ||
| * | ||
| * Valid keys have two segments: an optional prefix and a name. | ||
| * - Prefix: labels separated by dots, followed by `/`. Labels start with a letter, | ||
| * end with letter/digit, interior can be letters/digits/hyphens. | ||
| * Implementations SHOULD use reverse DNS notation (e.g., `com.example/`). | ||
| * - Name: starts/ends with alphanumeric, interior can be alphanumerics/hyphens/underscores/dots. | ||
| * May be empty if prefix is present. | ||
| * - Reserved: prefixes where the second label is `modelcontextprotocol` or `mcp` | ||
| * (e.g., `io.modelcontextprotocol/`, `dev.mcp/`). Note: `com.example.mcp/` is NOT reserved. | ||
| */ | ||
| export function validateMetaKey(key) { | ||
| if (!key) { | ||
| return { valid: false, reason: "key is empty" }; | ||
| } | ||
| const slashIndex = key.indexOf("/"); | ||
| let name; | ||
| if (slashIndex !== -1) { | ||
| const prefix = key.substring(0, slashIndex); | ||
| name = key.substring(slashIndex + 1); | ||
| // Validate prefix labels | ||
| const labels = prefix.split("."); | ||
| for (const label of labels) { | ||
| if (!label || !LABEL_PATTERN.test(label)) { | ||
| return { valid: false, reason: `invalid prefix label "${label}"` }; | ||
| } | ||
| } | ||
| // Check reserved prefixes: second label is "modelcontextprotocol" or "mcp" (per 2025-11-25 spec) | ||
| if (labels.length >= 2) { | ||
| const secondLabel = labels[1].toLowerCase(); | ||
| if (secondLabel === "modelcontextprotocol" || secondLabel === "mcp") { | ||
| return { valid: true, reserved: true, reason: `prefix "${prefix}/" is reserved for MCP protocol use` }; | ||
| } | ||
| } | ||
| } | ||
| else { | ||
| name = key; | ||
| } | ||
| // Validate name (empty name is valid per spec when prefix is present) | ||
| if (name && !NAME_PATTERN.test(name)) { | ||
| return { | ||
| valid: false, | ||
| reason: `must start/end with alphanumeric, contain only alphanumerics/hyphens/underscores/dots`, | ||
| }; | ||
| } | ||
| return { valid: true }; | ||
| } | ||
| /** | ||
| * Compute the qualified name for a skill by combining prefix and base name. | ||
@@ -162,3 +108,2 @@ * If prefix is empty, returns the base name unchanged. | ||
| const userInvocable = metadata["user-invocable"]; | ||
| const rawMetadata = metadata["metadata"]; | ||
| if (typeof name !== "string" || !name.trim()) { | ||
@@ -172,30 +117,2 @@ console.error(`Skill at ${skillDir}: missing or invalid 'name' field`); | ||
| } | ||
| // Validate metadata: must be a plain object if provided (Agent Skills spec: string keys to string values) | ||
| // Keys are validated against MCP _meta naming rules; invalid/reserved keys are warned and skipped. | ||
| let skillMetadata; | ||
| if (rawMetadata !== undefined && rawMetadata !== null) { | ||
| if (typeof rawMetadata === "object" && !Array.isArray(rawMetadata)) { | ||
| const validEntries = []; | ||
| for (const [k, v] of Object.entries(rawMetadata)) { | ||
| const keyStr = String(k); | ||
| const validation = validateMetaKey(keyStr); | ||
| if (!validation.valid) { | ||
| console.error(`Skill at ${skillDir}: skipping metadata key "${keyStr}": ${validation.reason}`); | ||
| continue; | ||
| } | ||
| if (validation.reserved) { | ||
| console.error(`Warning: Skill at ${skillDir}: skipping metadata key "${keyStr}": ${validation.reason}`); | ||
| continue; | ||
| } | ||
| // Coerce values to strings per Agent Skills spec | ||
| validEntries.push([keyStr, String(v)]); | ||
| } | ||
| if (validEntries.length > 0) { | ||
| skillMetadata = Object.fromEntries(validEntries); | ||
| } | ||
| } | ||
| else { | ||
| console.error(`Skill at ${skillDir}: 'metadata' must be a YAML mapping (key-value pairs), got ${Array.isArray(rawMetadata) ? "array" : typeof rawMetadata}`); | ||
| } | ||
| } | ||
| const effectiveAssistant = disableModelInvocation !== true; | ||
@@ -213,3 +130,2 @@ const effectiveUser = userInvocable !== false; | ||
| userInvocable: userInvocable !== false, // Default to true | ||
| metadata: skillMetadata, | ||
| // Initialize effective values from frontmatter (overrides applied later) | ||
@@ -329,3 +245,3 @@ effectiveAssistantInvocable: effectiveAssistant, | ||
| /** | ||
| * Compute MCP resource annotations for a skill. | ||
| * Compute MCP resource annotations and file size for a skill. | ||
| * Derives audience from effective invocation flags and sets default priority. | ||
@@ -335,3 +251,3 @@ * | ||
| * @param priority - Priority hint (0.0 = least important, 1.0 = most important, default 0.5) | ||
| * @returns Annotations object for use on MCP resources | ||
| * @returns Object with annotations and optional size (in bytes) for use on MCP resources | ||
| */ | ||
@@ -350,10 +266,12 @@ export function getResourceAnnotations(skill, priority = 0.5) { | ||
| const annotations = { audience, priority: clampedPriority }; | ||
| let size; | ||
| try { | ||
| const stat = fs.statSync(skill.path); | ||
| annotations.lastModified = stat.mtime.toISOString(); | ||
| size = stat.size; | ||
| } | ||
| catch { | ||
| // Skip lastModified if file cannot be stat'd | ||
| // Skip lastModified/size if file cannot be stat'd | ||
| } | ||
| return annotations; | ||
| return { annotations, size }; | ||
| } | ||
@@ -360,0 +278,0 @@ export const SKILL_COUNT_WARNING_THRESHOLD = 50; |
@@ -76,2 +76,3 @@ /** | ||
| for (const [name, skill] of skillState.skillMap) { | ||
| const { annotations, size } = getResourceAnnotations(skill, 0.3); | ||
| const resource = { | ||
@@ -82,6 +83,6 @@ uri: `skill://${encodeURIComponent(name)}/`, | ||
| description: `All files in ${name} skill directory`, | ||
| annotations: getResourceAnnotations(skill), | ||
| annotations, | ||
| }; | ||
| if (skill.metadata) { | ||
| resource._meta = skill.metadata; | ||
| if (size !== undefined) { | ||
| resource.size = size; | ||
| } | ||
@@ -161,2 +162,3 @@ resources.push(resource); | ||
| for (const [name, skill] of skillState.skillMap) { | ||
| const { annotations, size } = getResourceAnnotations(skill, 0.8); | ||
| const resource = { | ||
@@ -167,6 +169,6 @@ uri: `skill://${encodeURIComponent(name)}`, | ||
| description: skill.description, | ||
| annotations: getResourceAnnotations(skill), | ||
| annotations, | ||
| }; | ||
| if (skill.metadata) { | ||
| resource._meta = skill.metadata; | ||
| if (size !== undefined) { | ||
| resource.size = size; | ||
| } | ||
@@ -173,0 +175,0 @@ resources.push(resource); |
+3
-15
@@ -73,3 +73,3 @@ /** | ||
| const content = loadSkillContent(skill.path); | ||
| const result = { | ||
| return { | ||
| content: [ | ||
@@ -82,6 +82,2 @@ { | ||
| }; | ||
| if (skill.metadata) { | ||
| result._meta = skill.metadata; | ||
| } | ||
| return result; | ||
| } | ||
@@ -320,7 +316,3 @@ catch (error) { | ||
| } | ||
| const dirResult = { content: contents }; | ||
| if (skill.metadata) { | ||
| dirResult._meta = skill.metadata; | ||
| } | ||
| return dirResult; | ||
| return { content: contents }; | ||
| } | ||
@@ -356,3 +348,3 @@ // Check file size to prevent memory exhaustion | ||
| const content = fs.readFileSync(fullPath, "utf-8"); | ||
| const fileResult = { | ||
| return { | ||
| content: [ | ||
@@ -365,6 +357,2 @@ { | ||
| }; | ||
| if (skill.metadata) { | ||
| fileResult._meta = skill.metadata; | ||
| } | ||
| return fileResult; | ||
| } | ||
@@ -371,0 +359,0 @@ catch (error) { |
+2
-2
| { | ||
| "name": "@skilljack/mcp", | ||
| "version": "0.9.0", | ||
| "version": "0.10.0", | ||
| "description": "MCP server that discovers and serves Agent Skills. I know kung fu.", | ||
@@ -60,3 +60,3 @@ "type": "module", | ||
| "@modelcontextprotocol/ext-apps": "^1.0.0", | ||
| "@modelcontextprotocol/sdk": "^1.25.3", | ||
| "@modelcontextprotocol/sdk": "^1.29.0", | ||
| "chokidar": "^5.0.0", | ||
@@ -63,0 +63,0 @@ "simple-git": "^3.27.0", |
@@ -369,3 +369,2 @@ --- | ||
| | `user-invocable: false` | Yes | No | Background context (model auto-loads when relevant) | | ||
| | `metadata: { key: value }` | — | — | Arbitrary key-value pairs passed as `_meta` on MCP primitives | | ||
@@ -396,18 +395,2 @@ ### Example: User-Only Skill | ||
| ### Skill Metadata | ||
| The optional `metadata` frontmatter field allows attaching arbitrary key-value pairs to a skill, following the [Agent Skills spec](https://agentskills.io/specification). Values are coerced to strings. The metadata is translated to `_meta` on MCP resources and tool results. | ||
| ```yaml | ||
| --- | ||
| name: my-skill | ||
| description: A helpful skill | ||
| metadata: | ||
| author: example-org | ||
| version: "1.0" | ||
| --- | ||
| ``` | ||
| When this skill's resources are listed or its content is loaded via the `skill` or `skill-resource` tools, the response includes `_meta: { author: "example-org", version: "1.0" }`. | ||
| Note: Resources (`skill://` URIs) always include all skills regardless of visibility settings, allowing explicit access when needed. | ||
@@ -414,0 +397,0 @@ |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
992215
-0.46%5269
-1.99%