001/* 002 * Copyright 2022-2026 Revetware LLC. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package com.soklet; 017 018import com.soklet.internal.mcp.skills.McpSkillRuntimeBridge; 019import org.jspecify.annotations.NonNull; 020import org.jspecify.annotations.Nullable; 021 022import javax.annotation.concurrent.ThreadSafe; 023import java.util.Map; 024import java.util.Optional; 025import java.util.Set; 026 027/** 028 * Immutable snapshot of application-supplied Skills files and document metadata. 029 * 030 * <p>Logical file paths are bundle-relative names, not filesystem locations. 031 * Construction performs no file access, remote fetching, execution, or endpoint 032 * publication. Applications own loading the bytes before supplying them here. 033 * Metadata and path getters return cached immutable views; only file-byte 034 * inspection creates defensive copies. 035 * 036 * @author <a href="https://www.revetkn.com">Mark Allen</a> 037 */ 038@ThreadSafe 039public final class McpSkillBundle { 040 private final McpSkillRuntimeBridge.@NonNull Bundle runtimeBundle; 041 042 private McpSkillBundle(McpSkillRuntimeBridge.@NonNull Bundle runtimeBundle) { 043 this.runtimeBundle = runtimeBundle; 044 } 045 046 /** 047 * Validates and snapshots a bundle, including its required root {@code SKILL.md}. 048 * 049 * <p>Keys must be unique NFC logical paths with no empty, dot, or dot-dot 050 * segments, backslashes, percent signs, or control characters. A file cannot 051 * also be a directory prefix of another file (for example, {@code ref} and 052 * {@code ref/notes.md}). Names are case-sensitive and are not case-folded; 053 * applications loading or materializing files on a case-insensitive filesystem 054 * must account for names that differ only by case. Each path is 055 * limited to 8,192 UTF-8 bytes. Bundles contain at most 512 files and 16 MiB of 056 * raw content; individual resource representations remain subject to the 057 * stricter MCP output limits. Supplied arrays are defensively copied. Do not 058 * mutate the map or its arrays while construction is in progress. 059 * 060 * <p>The complete root document, including its body, must be valid UTF-8 and 061 * fit within 4 MiB. Frontmatter parsing and resolution share fixed bounds: 062 * nesting depth 128; 1,000,000 syntax and resolved nodes; 1,048,576 UTF-16 code 063 * units per scalar; 4,194,304 aggregate charged scalar characters; and 064 * 50,000,000 metered work units. The production JSON profile independently 065 * constrains resolved metadata and resource delivery. These construction 066 * bounds are not configurable through this factory. 067 * 068 * <p>Metadata validation errors identify the fixed field and violated rule 069 * without including authored values or custom metadata keys. The exception 070 * remains an {@link IllegalArgumentException}; diagnostics are not a structured 071 * validation API. Metadata validation does not repair or rewrite the document. 072 * 073 * @param fileContentsByLogicalPath logical file paths and complete file contents 074 * @return immutable validated bundle 075 * @throws NullPointerException if the map, a path, or file bytes are null 076 * @throws IllegalArgumentException if paths, metadata, content, or limits are invalid 077 */ 078 @NonNull 079 public static McpSkillBundle fromFiles( 080 @NonNull Map<@NonNull String, byte @NonNull []> fileContentsByLogicalPath) { 081 return new McpSkillBundle(McpSkillRuntimeBridge.fromFiles(fileContentsByLogicalPath)); 082 } 083 084 /** @return validated skill name from the root document */ 085 @NonNull 086 public String getName() { return this.runtimeBundle.name(); } 087 088 /** @return skill description from the root document */ 089 @NonNull 090 public String getDescription() { return this.runtimeBundle.description(); } 091 092 /** 093 * Returns cached root frontmatter, including supported unknown fields. 094 * 095 * @return immutable complete document metadata, not protocol {@code _meta} 096 */ 097 @NonNull 098 public McpJsonObject getDocumentMetadata() { return this.runtimeBundle.documentMetadata(); } 099 100 /** 101 * Returns cached canonical logical paths without filesystem resolution. 102 * 103 * @return immutable ordered set, root {@code SKILL.md} first and remaining 104 * paths in unsigned UTF-8 byte order 105 */ 106 @NonNull 107 public Set<@NonNull String> getFilePaths() { return this.runtimeBundle.filePaths(); } 108 109 /** 110 * Copies the requested file without copying the complete bundle. 111 * 112 * @param logicalFilePath exact, case-sensitive logical file path 113 * @return defensive file-byte copy, or empty when the path is absent 114 * @throws NullPointerException if the path is null 115 */ 116 @NonNull 117 public Optional<byte @NonNull []> findFileBytes(@NonNull String logicalFilePath) { 118 return this.runtimeBundle.findFileBytes(logicalFilePath); 119 } 120 121 McpSkillRuntimeBridge.@NonNull Bundle runtimeBundle() { return this.runtimeBundle; } 122 123 /** @return whether logical paths and exact snapshot bytes match */ 124 @Override 125 public boolean equals(@Nullable Object other) { 126 return this == other || other instanceof McpSkillBundle bundle 127 && this.runtimeBundle.equals(bundle.runtimeBundle); 128 } 129 130 /** @return structural bundle hash code without copying inspection bytes */ 131 @Override 132 public int hashCode() { return this.runtimeBundle.hashCode(); } 133 134 /** @return diagnostic rendering without authored metadata, paths, or bytes */ 135 @Override 136 @NonNull 137 public String toString() { return "McpSkillBundle[redacted]"; } 138}