diff --git a/.gitignore b/.gitignore index ec20709..ca98aa1 100644 --- a/.gitignore +++ b/.gitignore @@ -88,8 +88,6 @@ typings/ # Claude files .claude.json .claude/ -CLAUDE.md -PROJECT_STATUS.md # Vite .vite/ @@ -109,8 +107,4 @@ self-signed-key.pem invalid-json.json **/server/lib/** -# Rust example build artifacts -examples/calculator-rust/target/ -examples/calculator-rust/server/ - .yarn/install-state.gz \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 73dc7c2..14ac857 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,5 @@ "eslint.validate": ["typescript"], "[typescript]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" - }, - "typescript.tsdk": "node_modules/typescript/lib" + } } diff --git a/CLI.md b/CLI.md index 247f211..02787f8 100644 --- a/CLI.md +++ b/CLI.md @@ -292,8 +292,7 @@ MCPB uses PKCS#7 (Cryptographic Message Syntax) for digital signatures: ``` [Original MCPB ZIP content] MCPB_SIG_V1 -[4-byte little-endian length prefix] -[DER-encoded PKCS#7 signature] +[Base64-encoded PKCS#7 signature] MCPB_SIG_END ``` diff --git a/LICENSE b/LICENSE index 4a93985..8f30a63 100644 --- a/LICENSE +++ b/LICENSE @@ -1,216 +1,7 @@ -The MCP project is undergoing a licensing transition from the MIT License to the Apache License, Version 2.0 ("Apache-2.0"). All new code and specification contributions to the project are licensed under Apache-2.0. Documentation contributions (excluding specifications) are licensed under CC-BY-4.0. +Copyright 2025 Anthropic, PBC. -Contributions for which relicensing consent has been obtained are licensed under Apache-2.0. Contributions made by authors who originally licensed their work under the MIT License and who have not yet granted explicit permission to relicense remain licensed under the MIT License. +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: -No rights beyond those granted by the applicable original license are conveyed for such contributions. +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. ---- - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to the Licensor for inclusion in the Work by the copyright - owner or by an individual or Legal Entity authorized to submit on behalf - of the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - ---- - -MIT License - -Copyright (c) 2024-2025 Model Context Protocol a Series of LF Projects, LLC. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - ---- - -Creative Commons Attribution 4.0 International (CC-BY-4.0) - -Documentation in this project (excluding specifications) is licensed under -CC-BY-4.0. See https://creativecommons.org/licenses/by/4.0/legalcode for -the full license text. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/MANIFEST.md b/MANIFEST.md index 8ba7fa2..0358799 100644 --- a/MANIFEST.md +++ b/MANIFEST.md @@ -1,7 +1,7 @@ # MCPB Manifest.json Spec Current version: `0.3` -Last updated: 2025-12-02 +Last updated: 2025-10-24 ## Manifest Schema @@ -84,7 +84,7 @@ A full `manifest.json` with most of the optional fields looks like this: }, "repository": { "type": "git", - "url": "https://github.com/your-username/my-mcp-extension.git" + "url": "https://github.com/your-username/my-mcp-extension" }, "homepage": "https://example.com/my-extension", "documentation": "https://docs.example.com/my-extension", @@ -398,53 +398,6 @@ The `server` object defines how to run the MCP server: ### Server Types -Four server types are supported: - -- **`node`**: Node.js server with bundled dependencies -- **`python`**: Python server with bundled dependencies -- **`binary`**: Compiled executable -- **`uv`**: Python server using UV runtime (v0.4+) - -### UV Runtime (v0.4+) - -The `uv` server type enables cross-platform Python extensions without bundling dependencies. Instead, dependencies are declared in `pyproject.toml` and installed by the host application using UV. - -**Benefits:** -- Cross-platform support (Windows, macOS, Linux; Intel, ARM) -- Small bundle size (~100 KB vs 5-10 MB) -- Handles compiled dependencies (pydantic, numpy, etc.) -- No user Python installation required - -**Example:** -```json -{ - "manifest_version": "0.4", - "server": { - "type": "uv", - "entry_point": "src/server.py" - } -} -``` - -**Requirements:** -- Must include `pyproject.toml` with dependencies -- Must NOT include `server/lib/` or `server/venv/` -- `mcp_config` is optional (host manages execution) - -**Package structure:** -``` -extension.mcpb -├── manifest.json # server.type = "uv" -├── pyproject.toml # Dependencies -├── .mcpbignore # Exclude .venv, server/lib -└── src/ - └── server.py -``` - -See `examples/hello-world-uv` for a complete example. - -### Node, Python, and Binary Types - 1. **Python**: `server.type = "python"` - Requires `entry_point` to Python file - All dependencies must be bundled in the MCPB diff --git a/README.md b/README.md index d9f1fbe..608023e 100644 --- a/README.md +++ b/README.md @@ -109,22 +109,12 @@ bundle.mcpb (ZIP file) ### Bundling Dependencies -**UV Runtime (v0.4+):** +**Python Bundles:** -- Use `server.type = "uv"` in manifest -- Include `pyproject.toml` with dependencies (no bundled packages needed) -- Host application manages Python and dependencies automatically -- Works cross-platform without user Python installation -- See `examples/hello-world-uv` - -**Python Bundles (Traditional):** - -- Use `server.type = "python"` in manifest - Bundle all required packages in `server/lib/` directory - OR bundle a complete virtual environment in `server/venv/` - Use tools like `pip-tools`, `poetry`, or `pipenv` to create reproducible bundles - Set `PYTHONPATH` to include bundled packages via `mcp_config.env` -- **Limitation**: Cannot portably bundle compiled dependencies (e.g., pydantic, which the MCP Python SDK requires) **Node.js Bundles:** @@ -169,4 +159,4 @@ yarn test # License -This project is licensed under the Apache License 2.0 for new contributions, with existing code under MIT - see the [LICENSE](LICENSE) file for details. +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. diff --git a/examples/README.md b/examples/README.md index 334e531..24480ed 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,12 +14,11 @@ But, the MCP servers themselves are not robust secure production ready servers a ## Examples Included -| Example | Type | Demonstrates | -| --------------------- | ------- | ------------------------------------------- | -| `hello-world-node` | Node.js | Basic MCP server with simple time tool | -| `chrome-applescript` | Node.js | Browser automation via AppleScript | -| `file-manager-python` | Python | File system operations and path handling | -| `calculator-rust` | Binary | Compiled Rust binary as MCP Bundle | +| Example | Type | Demonstrates | +| --------------------- | ------- | ---------------------------------------- | +| `hello-world-node` | Node.js | Basic MCP server with simple time tool | +| `chrome-applescript` | Node.js | Browser automation via AppleScript | +| `file-manager-python` | Python | File system operations and path handling | ## Usage diff --git a/examples/calculator-rust/.mcpbignore b/examples/calculator-rust/.mcpbignore deleted file mode 100644 index 29c73c3..0000000 --- a/examples/calculator-rust/.mcpbignore +++ /dev/null @@ -1,5 +0,0 @@ -Cargo.toml -Cargo.lock -src/ -target/ -README.md diff --git a/examples/calculator-rust/Cargo.toml b/examples/calculator-rust/Cargo.toml deleted file mode 100644 index dfbca7e..0000000 --- a/examples/calculator-rust/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "mcp-calculator" -version = "1.0.0" -edition = "2024" - -[dependencies] -rmcp = { version = "1.2.0", features = ["server", "macros", "transport-io", "schemars"] } -tokio = { version = "1", features = ["macros", "rt-multi-thread", "io-std"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -anyhow = "1.0" -tracing = "0.1" -tracing-subscriber = { version = "0.3", features = ["env-filter", "std", "fmt"] } -schemars = "1.0" - -[profile.release] -strip = true -lto = true diff --git a/examples/calculator-rust/README.md b/examples/calculator-rust/README.md deleted file mode 100644 index 1482131..0000000 --- a/examples/calculator-rust/README.md +++ /dev/null @@ -1,73 +0,0 @@ -# Calculator Rust Binary Example - -Demonstrates packaging a compiled Rust binary as an MCP Bundle using the `binary` server type. Based on the [official Rust MCP SDK calculator example](https://github.com/modelcontextprotocol/rust-sdk/tree/main/examples/servers). - -## What is Binary Server Type? - -Unlike `node` or `uv` server types where the source code is bundled and executed by a runtime, the `binary` server type packages a pre-compiled native executable. This is useful for: - -- Rust, Go, C/C++ MCP servers -- Performance-sensitive workloads -- Servers with no runtime dependencies - -The tradeoff is that binaries are platform-specific — you need a separate build for each target OS/architecture. - -## Structure - -``` -calculator-rust/ -├── manifest.json # server.type = "binary" -├── Cargo.toml # Rust project -├── .mcpbignore # Exclude source from bundle -└── src/ - └── main.rs # Calculator MCP server (~80 LOC) -``` - -After building, the `server/` directory is created with the compiled binary. - -## Building - -Requires [Rust](https://rustup.rs/) 1.85+. - -```bash -cd examples/calculator-rust -cargo build --release -mkdir -p server -cp target/release/mcp-calculator server/ -``` - -## Packing - -```bash -mcpb pack examples/calculator-rust -``` - -The `.mcpbignore` file excludes source code and build artifacts — only `manifest.json` and `server/mcp-calculator` end up in the bundle. - -## Testing - -```bash -# MCP protocol handshake -echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' \ - | ./server/mcp-calculator - -# Tool call -(printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}\n' -printf '{"jsonrpc":"2.0","method":"notifications/initialized"}\n' -printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"sum","arguments":{"a":3,"b":4}}}\n' -sleep 1) | ./server/mcp-calculator -``` - -Expected: `sum(3, 4)` returns `{"text":"7"}`. - -## Tools - -- **sum** — Calculate the sum of two numbers -- **sub** — Calculate the difference of two numbers - -## Notes - -- Binary size: ~2.5 MB (stripped, LTO enabled) -- The `Cargo.toml` uses `edition = "2024"` (Rust 1.85+) -- Logs are written to stderr via `tracing`, so they don't interfere with MCP's stdio transport -- Uses the [rmcp](https://crates.io/crates/rmcp) crate (official Rust MCP SDK) diff --git a/examples/calculator-rust/manifest.json b/examples/calculator-rust/manifest.json deleted file mode 100644 index fd31518..0000000 --- a/examples/calculator-rust/manifest.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "$schema": "../../dist/mcpb-manifest.schema.json", - "manifest_version": "0.3", - "name": "calculator-rust", - "display_name": "Calculator (Rust Binary)", - "version": "1.0.0", - "description": "A simple calculator MCP server compiled from Rust", - "long_description": "Demonstrates packaging a compiled Rust binary as an MCP Bundle using the binary server type. The server provides sum and sub tools for basic arithmetic. Based on the official Rust MCP SDK calculator example.", - "author": { - "name": "Model Context Protocol" - }, - "server": { - "type": "binary", - "entry_point": "server/mcp-calculator", - "mcp_config": { - "command": "${__dirname}/server/mcp-calculator", - "args": [], - "env": {} - } - }, - "tools": [ - { - "name": "sum", - "description": "Calculate the sum of two numbers" - }, - { - "name": "sub", - "description": "Calculate the difference of two numbers" - } - ], - "keywords": ["calculator", "rust", "binary", "example"], - "license": "Apache-2.0", - "compatibility": { - "platforms": ["darwin", "linux"] - } -} diff --git a/examples/calculator-rust/src/main.rs b/examples/calculator-rust/src/main.rs deleted file mode 100644 index 4a0f885..0000000 --- a/examples/calculator-rust/src/main.rs +++ /dev/null @@ -1,80 +0,0 @@ -use anyhow::Result; -use rmcp::{ - ServerHandler, ServiceExt, - handler::server::{router::tool::ToolRouter, wrapper::Parameters}, - model::{ServerCapabilities, ServerInfo}, - schemars, tool, tool_handler, tool_router, - transport::stdio, -}; -use tracing_subscriber::{self, EnvFilter}; - -// --- Calculator types --- - -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct SumRequest { - #[schemars(description = "the left hand side number")] - pub a: i32, - pub b: i32, -} - -#[derive(Debug, serde::Deserialize, schemars::JsonSchema)] -pub struct SubRequest { - #[schemars(description = "the left hand side number")] - pub a: i32, - #[schemars(description = "the right hand side number")] - pub b: i32, -} - -// --- Calculator server --- - -#[derive(Debug, Clone)] -pub struct Calculator { - tool_router: ToolRouter, -} - -#[tool_router] -impl Calculator { - pub fn new() -> Self { - Self { - tool_router: Self::tool_router(), - } - } - - #[tool(description = "Calculate the sum of two numbers")] - fn sum(&self, Parameters(SumRequest { a, b }): Parameters) -> String { - (a + b).to_string() - } - - #[tool(description = "Calculate the difference of two numbers")] - fn sub(&self, Parameters(SubRequest { a, b }): Parameters) -> String { - (a - b).to_string() - } -} - -#[tool_handler] -impl ServerHandler for Calculator { - fn get_info(&self) -> ServerInfo { - ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) - .with_instructions("A simple calculator".to_string()) - } -} - -// --- Main --- - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::fmt() - .with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into())) - .with_writer(std::io::stderr) - .with_ansi(false) - .init(); - - tracing::info!("Starting Calculator MCP server"); - - let service = Calculator::new().serve(stdio()).await.inspect_err(|e| { - tracing::error!("serving error: {:?}", e); - })?; - - service.waiting().await?; - Ok(()) -} diff --git a/examples/chrome-applescript/server/index.js b/examples/chrome-applescript/server/index.js index 4f19b11..dd96bdd 100644 --- a/examples/chrome-applescript/server/index.js +++ b/examples/chrome-applescript/server/index.js @@ -33,6 +33,17 @@ class ChromeControlServer { this.setupHandlers(); } + // Helper methods + escapeForAppleScript(str) { + if (typeof str !== "string") return str; + // Basic AppleScript string escaping + return str + .replace(/\\/g, "\\\\") // Escape backslashes first + .replace(/"/g, '\\"') // Then escape double quotes + .replace(/\n/g, "\\n") // Escape newlines + .replace(/\r/g, "\\r"); // Escape carriage returns + } + async checkChromeAvailable() { try { const script = 'tell application "Google Chrome" to return "available"'; @@ -71,28 +82,12 @@ class ChromeControlServer { } } - // Coerce tab_id to a safe numeric value before interpolating into - // AppleScript. inputSchema type:"number" is advisory — runtime may receive - // a string, which would otherwise be injected into the osascript template. - toSafeTabId(tabId) { - if (tabId === null || tabId === undefined) return null; - const n = Number.parseInt(tabId, 10); - if (Number.isNaN(n)) { - throw new Error("tab_id must be a number"); - } - return n; - } - async findTabById(tabId) { - const safeTabId = this.toSafeTabId(tabId); - if (safeTabId === null) { - return { w: null, t: null, found: false }; - } const script = ` tell application "Google Chrome" repeat with w in windows repeat with t in tabs of w - if (id of t as string) is "${safeTabId}" then + if (id of t as string) is "${tabId}" then return {w, t, true} end if end repeat @@ -108,8 +103,7 @@ class ChromeControlServer { operation, successMessage = "Operation completed", ) { - const safeTabId = this.toSafeTabId(tabId); - if (safeTabId === null) { + if (!tabId) { // No tab ID, execute on active tab return this.executeAppleScript(operation.activeScript); } @@ -118,7 +112,7 @@ class ChromeControlServer { tell application "Google Chrome" repeat with w in windows repeat with t in tabs of w - if (id of t as string) is "${safeTabId}" then + if (id of t as string) is "${tabId}" then ${operation.tabScript} return "${successMessage}" end if @@ -278,23 +272,10 @@ class ChromeControlServer { throw new Error("URL is required and must be a string"); } - try { - new URL(url); - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error: url is not a valid url - ${error.message}`, - }, - ], - isError: true, - }; - } - + const escapedUrl = this.escapeForAppleScript(url); const script = new_tab - ? `tell application "Google Chrome" to open location ${JSON.stringify(url)}` - : `tell application "Google Chrome" to set URL of active tab of front window to ${JSON.stringify(url)}`; + ? `tell application "Google Chrome" to open location "${escapedUrl}"` + : `tell application "Google Chrome" to set URL of active tab of front window to "${escapedUrl}"`; await this.executeAppleScript(script); return { @@ -570,7 +551,6 @@ class ChromeControlServer { "JavaScript code is required and must be a string", ); } - const safeTabId = this.toSafeTabId(tab_id); // Wrap the code to capture console.log outputs const wrappedCode = ` @@ -646,14 +626,15 @@ class ChromeControlServer { })(); `; - const script = - safeTabId != null - ? ` + const escapedCode = this.escapeForAppleScript(wrappedCode); + + const script = tab_id + ? ` tell application "Google Chrome" repeat with w in windows repeat with t in tabs of w - if (id of t as string) is "${safeTabId}" then - set result to execute t javascript ${JSON.stringify(wrappedCode)} + if (id of t as string) is "${tab_id}" then + set result to execute t javascript "${escapedCode}" return result end if end repeat @@ -661,9 +642,9 @@ class ChromeControlServer { return "Tab not found" end tell ` - : ` + : ` tell application "Google Chrome" - execute active tab of front window javascript ${JSON.stringify(wrappedCode)} + execute active tab of front window javascript "${escapedCode}" end tell `; @@ -714,16 +695,14 @@ class ChromeControlServer { case "get_page_content": { const { tab_id } = args; - const safeTabId = this.toSafeTabId(tab_id); const jsCode = "document.body.innerText"; - const script = - safeTabId != null - ? ` + const script = tab_id + ? ` tell application "Google Chrome" repeat with w in windows repeat with t in tabs of w - if (id of t as string) is "${safeTabId}" then + if (id of t as string) is "${tab_id}" then set pageContent to execute t javascript "${jsCode}" return pageContent end if @@ -732,7 +711,7 @@ class ChromeControlServer { return "Tab not found" end tell ` - : ` + : ` tell application "Google Chrome" execute active tab of front window javascript "${jsCode}" end tell diff --git a/examples/file-manager-python/.mcpbignore b/examples/file-manager-python/.mcpbignore deleted file mode 100644 index f753254..0000000 --- a/examples/file-manager-python/.mcpbignore +++ /dev/null @@ -1,7 +0,0 @@ -.venv/ -__pycache__/ -*.pyc -.pytest_cache/ -.mypy_cache/ -*.egg-info/ -uv.lock diff --git a/examples/file-manager-python/manifest.json b/examples/file-manager-python/manifest.json index 69b43e2..964b2aa 100644 --- a/examples/file-manager-python/manifest.json +++ b/examples/file-manager-python/manifest.json @@ -1,28 +1,28 @@ { - "manifest_version": "0.4", + "$schema": "../../dist/mcpb-manifest.schema.json", + "manifest_version": "0.1", "name": "file-manager-python", "display_name": "Python File Manager MCP", - "version": "0.2.0", + "version": "0.1.0", "description": "A Python MCP server for file operations", - "long_description": "This extension provides file management capabilities through a Python MCP server. It demonstrates Python-based MCP Bundle development using UV runtime, including file operations, directory management, and proper MCP protocol implementation.", + "long_description": "This extension provides file management capabilities through a Python MCP server. It demonstrates Python-based MCP Bundle development, including file operations, directory management, and proper MCP protocol implementation.", "author": { "name": "Anthropic", "email": "support@anthropic.com", "url": "https://github.com/anthropics" }, "server": { - "type": "uv", + "type": "python", "entry_point": "server/main.py", "mcp_config": { - "command": "uv", + "command": "python", "args": [ - "run", - "--directory", "${__dirname}", - "server/main.py", + "${__dirname}/server/main.py", "--workspace=${user_config.workspace_directory}" ], "env": { - "DEBUG": "${user_config.debug_mode}" + "DEBUG": "${user_config.debug_mode}", + "PYTHONPATH": "${__dirname}/server/lib" } } }, @@ -62,7 +62,8 @@ "claude_desktop": ">=0.10.0", "platforms": ["darwin", "win32", "linux"], "runtimes": { - "python": ">=3.10.0 <4" + "python": ">=3.8.0 <4" } - } + }, + "privacy_policies": [] } diff --git a/examples/file-manager-python/pyproject.toml b/examples/file-manager-python/pyproject.toml index 5a1e799..7895f32 100644 --- a/examples/file-manager-python/pyproject.toml +++ b/examples/file-manager-python/pyproject.toml @@ -1,8 +1,31 @@ +[build-system] +requires = ["setuptools>=42", "wheel"] +build-backend = "setuptools.build_meta" + [project] name = "file-manager-python" -version = "0.2.0" +version = "0.1.0" description = "A Python MCP server for file operations" -requires-python = ">=3.10" -dependencies = [ - "mcp>=1.0.0", +authors = [ + {name = "Anthropic", email = "support@anthropic.com"} ] +license = {text = "MIT"} +classifiers = [ + "Private :: Do Not Upload" # Prevents accidental uploads to PyPI +] + +[project.dependencies] +mcp = ">=1.0.0" +trio = ">=0.22.0" + +[tool.setuptools] +packages = ["server"] + +[project.scripts] +bundle-deps = "python:install_deps" + +# Custom scripts section for DXT development +[tool.dxt] +bundle = "pip install \"mcp[all]\" --target server/lib --upgrade --force-reinstall" +test = "python server/main.py --debug" +pack = "npx @anthropic-ai/dxt pack" \ No newline at end of file diff --git a/examples/file-manager-python/requirements.txt b/examples/file-manager-python/requirements.txt new file mode 100644 index 0000000..d90d65c --- /dev/null +++ b/examples/file-manager-python/requirements.txt @@ -0,0 +1,2 @@ +mcp>=1.0.0 +trio>=0.22.0 diff --git a/examples/file-system-node/package.json b/examples/file-system-node/package.json index 78173f8..e810564 100644 --- a/examples/file-system-node/package.json +++ b/examples/file-system-node/package.json @@ -1,10 +1,10 @@ { "name": "ant.dir.ant.anthropic.filesystem", - "version": "0.1.4", + "version": "0.1.3", "type": "module", "private": true, "main": "server/index.js", "dependencies": { - "@modelcontextprotocol/server-filesystem": "2025.11.25" + "@modelcontextprotocol/server-filesystem": "2025.1.14" } } diff --git a/examples/hello-world-node/server/index.js b/examples/hello-world-node/server/index.js index 1d68293..03efb72 100644 --- a/examples/hello-world-node/server/index.js +++ b/examples/hello-world-node/server/index.js @@ -64,13 +64,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => { }); // Start the server -async function main() { - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error("Hello World MCP server running..."); -} +const transport = new StdioServerTransport(); +server.connect(transport); -main().catch((error) => { - console.error("Fatal error in main():", error); - process.exit(1); -}); +console.error("Hello World MCP server running..."); diff --git a/examples/hello-world-uv/.mcpbignore b/examples/hello-world-uv/.mcpbignore deleted file mode 100644 index eec9004..0000000 --- a/examples/hello-world-uv/.mcpbignore +++ /dev/null @@ -1,8 +0,0 @@ -.venv/ -__pycache__/ -*.pyc -.pytest_cache/ -.mypy_cache/ -*.egg-info/ -server/lib/ -server/venv/ diff --git a/examples/hello-world-uv/README.md b/examples/hello-world-uv/README.md deleted file mode 100644 index 853bb06..0000000 --- a/examples/hello-world-uv/README.md +++ /dev/null @@ -1,60 +0,0 @@ -# Hello World UV Runtime Example - -This example demonstrates a minimal MCP server using **UV runtime**. - -## What is UV Runtime? - -UV runtime lets Claude Desktop automatically manage Python and dependencies for your extension: -- Downloads the correct Python version for the user's platform -- Creates an isolated virtual environment -- Installs dependencies from `pyproject.toml` -- Works cross-platform (Windows, macOS, Linux) without user setup - -## Structure - -``` -hello-world-uv/ -├── manifest.json # server.type = "uv" -├── pyproject.toml # Dependencies listed here -├── .mcpbignore # Exclude build artifacts -└── src/ - └── server.py # MCP server implementation -``` - -## Key Differences from Python Runtime - -**UV Runtime** (this example): -- `server.type = "uv"` -- No bundled dependencies -- `mcp_config` uses `uv run` to auto-resolve deps from `pyproject.toml` -- Small bundle size (~2 KB) -- Works on any platform - -**Python Runtime** (traditional): -- `server.type = "python"` -- Must bundle dependencies in `server/lib/` -- Requires `mcp_config` with PYTHONPATH -- Larger bundle size -- Only works with pure Python (no compiled deps) - -## Installing - -```bash -mcpb pack -``` - -Install the generated `.mcpb` file in Claude Desktop. - -## Testing Locally - -```bash -# Install dependencies -uv sync - -# Run server -uv run src/server.py -``` - -## Tools - -- **say_hello** - Greets a person by name diff --git a/examples/hello-world-uv/icon.png b/examples/hello-world-uv/icon.png deleted file mode 100644 index 08cd6f2..0000000 Binary files a/examples/hello-world-uv/icon.png and /dev/null differ diff --git a/examples/hello-world-uv/manifest.json b/examples/hello-world-uv/manifest.json deleted file mode 100644 index 534ef4d..0000000 --- a/examples/hello-world-uv/manifest.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "manifest_version": "0.4", - "name": "hello-world-uv", - "display_name": "Hello World (UV Runtime)", - "version": "1.0.0", - "description": "Simple MCP server using UV runtime", - "author": { - "name": "Anthropic" - }, - "icon": "icon.png", - "server": { - "type": "uv", - "entry_point": "src/server.py", - "mcp_config": { - "command": "uv", - "args": ["run", "--directory", "${__dirname}", "src/server.py"] - } - }, - "compatibility": { - "platforms": ["darwin", "linux", "win32"], - "runtimes": { - "python": ">=3.10" - } - }, - "keywords": ["example", "hello-world", "uv"], - "license": "MIT" -} diff --git a/examples/hello-world-uv/pyproject.toml b/examples/hello-world-uv/pyproject.toml deleted file mode 100644 index 0eca14c..0000000 --- a/examples/hello-world-uv/pyproject.toml +++ /dev/null @@ -1,8 +0,0 @@ -[project] -name = "hello-world-uv" -version = "1.0.0" -description = "Simple MCP server using UV runtime" -requires-python = ">=3.10" -dependencies = [ - "mcp>=1.0.0", -] diff --git a/examples/hello-world-uv/src/server.py b/examples/hello-world-uv/src/server.py deleted file mode 100644 index ef871d2..0000000 --- a/examples/hello-world-uv/src/server.py +++ /dev/null @@ -1,65 +0,0 @@ -#!/usr/bin/env python3 -"""Hello World MCP server using UV runtime.""" - -import asyncio -from mcp.server.models import InitializationOptions -import mcp.types as types -from mcp.server import NotificationOptions, Server -import mcp.server.stdio - - -server = Server("hello-world-uv") - - -@server.list_tools() -async def handle_list_tools() -> list[types.Tool]: - """List available tools.""" - return [ - types.Tool( - name="say_hello", - description="Say hello to someone", - inputSchema={ - "type": "object", - "properties": { - "name": { - "type": "string", - "description": "Name to greet", - } - }, - "required": ["name"], - }, - ) - ] - - -@server.call_tool() -async def handle_call_tool( - name: str, arguments: dict | None -) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: - """Handle tool calls.""" - if name == "say_hello": - person_name = arguments.get("name", "World") if arguments else "World" - return [types.TextContent(type="text", text=f"Hello, {person_name}!")] - - raise ValueError(f"Unknown tool: {name}") - - -async def main(): - """Run the server.""" - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - await server.run( - read_stream, - write_stream, - InitializationOptions( - server_name="hello-world-uv", - server_version="1.0.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/package.json b/package.json index 09a33ee..8749e20 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anthropic-ai/mcpb", "description": "Tools for building MCP Bundles", - "version": "2.1.2", + "version": "1.1.5", "type": "module", "main": "dist/index.js", "module": "dist/index.js", @@ -89,7 +89,7 @@ "fflate": "^0.8.2", "galactus": "^1.0.0", "ignore": "^7.0.5", - "node-forge": "^1.3.2", + "node-forge": "^1.3.1", "pretty-bytes": "^5.6.0", "zod": "^3.25.67", "zod-to-json-schema": "^3.24.6" @@ -99,4 +99,4 @@ "@babel/parser": "7.27.3" }, "packageManager": "yarn@4.10.3+sha512.c38cafb5c7bb273f3926d04e55e1d8c9dfa7d9c3ea1f36a4868fa028b9e5f72298f0b7f401ad5eb921749eb012eb1c3bb74bf7503df3ee43fd600d14a018266f" -} +} \ No newline at end of file diff --git a/schemas/mcpb-manifest-v0.1.schema.json b/schemas/mcpb-manifest-v0.1.schema.json index 821a00a..17e6fd8 100644 --- a/schemas/mcpb-manifest-v0.1.schema.json +++ b/schemas/mcpb-manifest-v0.1.schema.json @@ -306,6 +306,13 @@ ], "additionalProperties": false } + }, + "_meta": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": {} + } } }, "required": [ diff --git a/schemas/mcpb-manifest-v0.4.schema.json b/schemas/mcpb-manifest-v0.4.schema.json deleted file mode 100644 index 5917415..0000000 --- a/schemas/mcpb-manifest-v0.4.schema.json +++ /dev/null @@ -1,377 +0,0 @@ -{ - "type": "object", - "properties": { - "$schema": { - "type": "string" - }, - "dxt_version": { - "type": "string", - "const": "0.4", - "description": "@deprecated Use manifest_version instead" - }, - "manifest_version": { - "type": "string", - "const": "0.4" - }, - "name": { - "type": "string" - }, - "display_name": { - "type": "string" - }, - "version": { - "type": "string" - }, - "description": { - "type": "string" - }, - "long_description": { - "type": "string" - }, - "author": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "email": { - "type": "string", - "format": "email" - }, - "url": { - "type": "string", - "format": "uri" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - }, - "repository": { - "type": "object", - "properties": { - "type": { - "type": "string" - }, - "url": { - "type": "string", - "format": "uri" - } - }, - "required": [ - "type", - "url" - ], - "additionalProperties": false - }, - "homepage": { - "type": "string", - "format": "uri" - }, - "documentation": { - "type": "string", - "format": "uri" - }, - "support": { - "type": "string", - "format": "uri" - }, - "icon": { - "type": "string" - }, - "icons": { - "type": "array", - "items": { - "type": "object", - "properties": { - "src": { - "type": "string" - }, - "size": { - "type": "string", - "pattern": "^\\d+x\\d+$" - }, - "theme": { - "type": "string", - "minLength": 1 - } - }, - "required": [ - "src", - "size" - ], - "additionalProperties": false - } - }, - "screenshots": { - "type": "array", - "items": { - "type": "string" - } - }, - "localization": { - "type": "object", - "properties": { - "resources": { - "type": "string", - "pattern": "\\$\\{locale\\}" - }, - "default_locale": { - "type": "string", - "pattern": "^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$" - } - }, - "required": [ - "resources", - "default_locale" - ], - "additionalProperties": false - }, - "server": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "python", - "node", - "binary", - "uv" - ] - }, - "entry_point": { - "type": "string" - }, - "mcp_config": { - "type": "object", - "properties": { - "command": { - "type": "string" - }, - "args": { - "type": "array", - "items": { - "type": "string" - } - }, - "env": { - "type": "object", - "additionalProperties": { - "type": "string" - } - }, - "platform_overrides": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "command": { - "$ref": "#/properties/server/properties/mcp_config/properties/command" - }, - "args": { - "$ref": "#/properties/server/properties/mcp_config/properties/args" - }, - "env": { - "$ref": "#/properties/server/properties/mcp_config/properties/env" - } - }, - "additionalProperties": false - } - } - }, - "required": [ - "command" - ], - "additionalProperties": false - } - }, - "required": [ - "type", - "entry_point", - "mcp_config" - ], - "additionalProperties": false - }, - "tools": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - } - }, - "required": [ - "name" - ], - "additionalProperties": false - } - }, - "tools_generated": { - "type": "boolean" - }, - "prompts": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": { - "type": "string" - }, - "description": { - "type": "string" - }, - "arguments": { - "type": "array", - "items": { - "type": "string" - } - }, - "text": { - "type": "string" - } - }, - "required": [ - "name", - "text" - ], - "additionalProperties": false - } - }, - "prompts_generated": { - "type": "boolean" - }, - "keywords": { - "type": "array", - "items": { - "type": "string" - } - }, - "license": { - "type": "string" - }, - "privacy_policies": { - "type": "array", - "items": { - "type": "string", - "format": "uri" - } - }, - "compatibility": { - "type": "object", - "properties": { - "claude_desktop": { - "type": "string" - }, - "platforms": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "darwin", - "win32", - "linux" - ] - } - }, - "runtimes": { - "type": "object", - "properties": { - "python": { - "type": "string" - }, - "node": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - "user_config": { - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": [ - "string", - "number", - "boolean", - "directory", - "file" - ] - }, - "title": { - "type": "string" - }, - "description": { - "type": "string" - }, - "required": { - "type": "boolean" - }, - "default": { - "anyOf": [ - { - "type": "string" - }, - { - "type": "number" - }, - { - "type": "boolean" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "multiple": { - "type": "boolean" - }, - "sensitive": { - "type": "boolean" - }, - "min": { - "type": "number" - }, - "max": { - "type": "number" - } - }, - "required": [ - "type", - "title", - "description" - ], - "additionalProperties": false - } - }, - "_meta": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": {} - } - } - }, - "required": [ - "name", - "version", - "description", - "author", - "server" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" -} \ No newline at end of file diff --git a/scripts/build-mcpb-schema.js b/scripts/build-mcpb-schema.js index 7f906eb..a62fcdb 100644 --- a/scripts/build-mcpb-schema.js +++ b/scripts/build-mcpb-schema.js @@ -1,8 +1,10 @@ +import { + McpbManifestSchema as McpbManifestSchemaLatest, + McpbSignatureInfoSchema, +} from "../dist/schemas/latest.js"; import { McpbManifestSchema as McpbManifestSchema_v0_1 } from "../dist/schemas/0.1.js"; import { McpbManifestSchema as McpbManifestSchema_v0_2 } from "../dist/schemas/0.2.js"; import { McpbManifestSchema as McpbManifestSchema_v0_3 } from "../dist/schemas/0.3.js"; -import { McpbManifestSchema as McpbManifestSchema_v0_4 } from "../dist/schemas/0.4.js"; -import { McpbSignatureInfoSchema } from "../dist/shared/common.js"; import { zodToJsonSchema } from "zod-to-json-schema"; import fs from "node:fs/promises"; import path from "node:path"; @@ -15,7 +17,7 @@ const versionedManifestSchemas = { "mcpb-manifest-v0.1": McpbManifestSchema_v0_1, "mcpb-manifest-v0.2": McpbManifestSchema_v0_2, "mcpb-manifest-v0.3": McpbManifestSchema_v0_3, - "mcpb-manifest-v0.4": McpbManifestSchema_v0_4, + "mcpb-manifest-latest": McpbManifestSchemaLatest, }; // Other schemas diff --git a/src/browser.ts b/src/browser.ts index 92c9ab8..d3f0598 100644 --- a/src/browser.ts +++ b/src/browser.ts @@ -1,6 +1,5 @@ // Browser-compatible exports -export * from "./schemas/index.js"; -export * from "./shared/common.js"; +export * from "./schemas/latest.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/cli.ts b/src/cli.ts index c5266d4..5dc861c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3,9 +3,10 @@ export * from "./cli/init.js"; export * from "./cli/pack.js"; // Include all shared exports -export * from "./schemas/index.js"; +export * from "./schemas/latest.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; +export * from "./types.js"; // Include node exports since CLI needs them export * from "./node/files.js"; diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 5ba121c..ac38b52 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -51,32 +51,19 @@ program .command("init [directory]") .description("Create a new MCPB extension manifest") .option("-y, --yes", "Accept all defaults (non-interactive mode)") - .option( - "--manifest-version ", - "Manifest version to use in the generated manifest", - ) - .action( - ( - directory?: string, - options?: { yes?: boolean; manifestVersion?: string }, - ) => { - void (async () => { - try { - const success = await initExtension( - directory, - options?.yes, - options?.manifestVersion, - ); - process.exit(success ? 0 : 1); - } catch (error) { - console.error( - `ERROR: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - process.exit(1); - } - })(); - }, - ); + .action((directory?: string, options?: { yes?: boolean }) => { + void (async () => { + try { + const success = await initExtension(directory, options?.yes); + process.exit(success ? 0 : 1); + } catch (error) { + console.error( + `ERROR: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + process.exit(1); + } + })(); + }); // Validate command program @@ -101,33 +88,22 @@ program program .command("pack [directory] [output]") .description("Pack a directory into an MCPB extension") - .option( - "-m, --manifest ", - "Path to manifest file (defaults to manifest.json in directory)", - ) - .action( - ( - directory: string = process.cwd(), - output: string | undefined, - options: { manifest?: string }, - ) => { - void (async () => { - try { - const success = await packExtension({ - extensionPath: directory, - outputPath: output, - manifestPath: options.manifest, - }); - process.exit(success ? 0 : 1); - } catch (error) { - console.error( - `ERROR: ${error instanceof Error ? error.message : "Unknown error"}`, - ); - process.exit(1); - } - })(); - }, - ); + .action((directory: string = process.cwd(), output?: string) => { + void (async () => { + try { + const success = await packExtension({ + extensionPath: directory, + outputPath: output, + }); + process.exit(success ? 0 : 1); + } catch (error) { + console.error( + `ERROR: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + process.exit(1); + } + })(); + }); // Unpack command program diff --git a/src/cli/init.ts b/src/cli/init.ts index f2dfb53..58a7317 100644 --- a/src/cli/init.ts +++ b/src/cli/init.ts @@ -2,11 +2,8 @@ import { confirm, input, select } from "@inquirer/prompts"; import { existsSync, readFileSync, writeFileSync } from "fs"; import { basename, join, resolve } from "path"; -import { - DEFAULT_MANIFEST_VERSION, - MANIFEST_SCHEMAS, -} from "../shared/constants.js"; -import type { McpbManifestAny } from "../types.js"; +import { LATEST_MANIFEST_VERSION } from "../shared/constants.js"; +import type { McpbManifestLatest } from "../types.js"; interface PackageJson { name?: string; @@ -877,19 +874,18 @@ export function buildManifest( license: string; repository?: { type: string; url: string }; }, - manifestVersion: keyof typeof MANIFEST_SCHEMAS = DEFAULT_MANIFEST_VERSION, - // localization?: { - // resources: string; - // default_locale: string; - // }, -): McpbManifestAny { + localization?: { + resources: string; + default_locale: string; + }, +): McpbManifestLatest { const { name, displayName, version, description, authorName } = basicInfo; const { authorEmail, authorUrl } = authorInfo; const { serverType, entryPoint, mcp_config } = serverConfig; const { keywords, license, repository } = optionalFields; return { - manifest_version: manifestVersion, + manifest_version: LATEST_MANIFEST_VERSION, name, ...(displayName && displayName !== name ? { display_name: displayName } @@ -910,7 +906,7 @@ export function buildManifest( ...(visualAssets.screenshots.length > 0 ? { screenshots: visualAssets.screenshots } : {}), - // ...(localization ? { localization } : {}), + ...(localization ? { localization } : {}), server: { type: serverType, entry_point: entryPoint, @@ -946,21 +942,10 @@ export function printNextSteps() { export async function initExtension( targetPath: string = process.cwd(), nonInteractive = false, - manifestVersion?: string, ): Promise { const resolvedPath = resolve(targetPath); const manifestPath = join(resolvedPath, "manifest.json"); - // Validate manifest version if provided - if (manifestVersion && !(manifestVersion in MANIFEST_SCHEMAS)) { - console.error( - `ERROR: Invalid manifest version "${manifestVersion}". Supported versions: ${Object.keys(MANIFEST_SCHEMAS).join(", ")}`, - ); - return false; - } - const effectiveManifestVersion = (manifestVersion || - DEFAULT_MANIFEST_VERSION) as keyof typeof MANIFEST_SCHEMAS; - if (existsSync(manifestPath)) { if (nonInteractive) { console.log( @@ -1006,9 +991,9 @@ export async function initExtension( const visualAssets = nonInteractive ? { icon: "", icons: [], screenshots: [] } : await promptVisualAssets(); - // const localization = nonInteractive - // ? undefined - // : await promptLocalization(); + const localization = nonInteractive + ? undefined + : await promptLocalization(); const serverConfig = nonInteractive ? getDefaultServerConfig(packageData) : await promptServerConfig(packageData); @@ -1041,7 +1026,7 @@ export async function initExtension( compatibility, userConfig, optionalFields, - effectiveManifestVersion, + localization, ); // Write manifest diff --git a/src/cli/pack.ts b/src/cli/pack.ts index c3ec099..58b3c1c 100644 --- a/src/cli/pack.ts +++ b/src/cli/pack.ts @@ -13,16 +13,17 @@ import { basename, join, relative, resolve, sep } from "path"; import { getAllFilesWithCount, readMcpbIgnorePatterns } from "../node/files.js"; import { validateManifest } from "../node/validate.js"; -import { MANIFEST_SCHEMAS } from "../shared/constants.js"; +import { + LATEST_MANIFEST_SCHEMA, + LATEST_MANIFEST_VERSION, +} from "../shared/constants.js"; import { getLogger } from "../shared/log.js"; -import { getManifestVersionFromRawData } from "../shared/manifestVersionResolve.js"; import { initExtension } from "./init.js"; interface PackOptions { extensionPath: string; outputPath?: string; silent?: boolean; - manifestPath?: string; } function formatFileSize(bytes: number): string { @@ -51,7 +52,6 @@ export async function packExtension({ extensionPath, outputPath, silent, - manifestPath: customManifestPath, }: PackOptions): Promise { const resolvedPath = resolve(extensionPath); const logger = getLogger({ silent }); @@ -62,18 +62,9 @@ export async function packExtension({ return false; } - // Resolve manifest path - const manifestPath = customManifestPath - ? resolve(customManifestPath) - : join(resolvedPath, "manifest.json"); - + // Check if manifest exists + const manifestPath = join(resolvedPath, "manifest.json"); if (!existsSync(manifestPath)) { - if (customManifestPath) { - // When --manifest is explicitly provided, error immediately - logger.error(`ERROR: Manifest file not found: ${customManifestPath}`); - return false; - } - logger.log(`No manifest.json found in ${extensionPath}`); const shouldInit = await confirm({ message: "Would you like to create a manifest.json file?", @@ -94,11 +85,7 @@ export async function packExtension({ // Validate manifest first logger.log("Validating manifest..."); - if ( - !validateManifest(manifestPath, { - projectDir: resolvedPath, - }) - ) { + if (!validateManifest(manifestPath)) { logger.error("ERROR: Cannot pack extension with invalid manifest"); return false; } @@ -108,18 +95,7 @@ export async function packExtension({ try { const manifestContent = readFileSync(manifestPath, "utf-8"); const manifestData = JSON.parse(manifestContent); - const manifestVersion = getManifestVersionFromRawData(manifestData); - if (!manifestVersion) { - logger.error( - `ERROR: Manifest version mismatch. Expected "${Object.keys(MANIFEST_SCHEMAS).join(" or ")}", found "${manifestVersion}"`, - ); - logger.error( - ` Please update the manifest_version in your manifest.json to a supported version`, - ); - return false; - } - - manifest = MANIFEST_SCHEMAS[manifestVersion].parse(manifestData); + manifest = LATEST_MANIFEST_SCHEMA.parse(manifestData); } catch (error) { logger.error("ERROR: Failed to parse manifest.json"); if (error instanceof Error) { @@ -128,6 +104,17 @@ export async function packExtension({ return false; } + const manifestVersion = manifest.manifest_version || manifest.dxt_version; + if (manifestVersion !== LATEST_MANIFEST_VERSION) { + logger.error( + `ERROR: Manifest version mismatch. Expected "${LATEST_MANIFEST_VERSION}", found "${manifestVersion}"`, + ); + logger.error( + ` Please update the manifest_version in your manifest.json to "${LATEST_MANIFEST_VERSION}"`, + ); + return false; + } + // Determine output path const extensionName = basename(resolvedPath); const finalOutputPath = outputPath @@ -150,15 +137,6 @@ export async function packExtension({ mcpbIgnorePatterns, ); - // When using a custom manifest path, inject the manifest into the bundle - if (customManifestPath) { - const manifestStat = statSync(manifestPath); - files["manifest.json"] = { - data: readFileSync(manifestPath), - mode: manifestStat.mode, - }; - } - // Print package header logger.log(`\n📦 ${manifest.name}@${manifest.version}`); @@ -226,22 +204,15 @@ export async function packExtension({ const zipFiles: Zippable = {}; const isUnix = process.platform !== "win32"; - const isBinaryType = manifest.server?.type === "binary"; - const binaryEntryPoint = isBinaryType ? manifest.server.entry_point : null; for (const [filePath, fileData] of Object.entries(files)) { if (isUnix) { - let mode = fileData.mode & 0o777; - // Ensure binary entry points are always executable in the ZIP, - // regardless of filesystem permissions. Windows-built ZIPs store - // 644 because Windows has no Unix execute bit — CD spawns binary - // entry points directly, so they must have +x to avoid EACCES. - if (binaryEntryPoint && filePath === binaryEntryPoint) { - mode = mode | 0o111; - } // Set external file attributes to preserve Unix permissions // The mode needs to be shifted to the upper 16 bits for ZIP format - zipFiles[filePath] = [fileData.data, { os: 3, attrs: mode << 16 }]; + zipFiles[filePath] = [ + fileData.data, + { os: 3, attrs: (fileData.mode & 0o777) << 16 }, + ]; } else { // On Windows, use default ZIP attributes (no Unix permissions) zipFiles[filePath] = fileData.data; diff --git a/src/index.ts b/src/index.ts index ca55908..62ada47 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,8 +5,7 @@ export * from "./cli/unpack.js"; export * from "./node/files.js"; export * from "./node/sign.js"; export * from "./node/validate.js"; -export * from "./schemas/index.js"; -export * from "./shared/common.js"; +export * from "./schemas/latest.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; export * from "./types.js"; diff --git a/src/node.ts b/src/node.ts index f7f48a0..12a7edb 100644 --- a/src/node.ts +++ b/src/node.ts @@ -4,6 +4,7 @@ export * from "./node/sign.js"; export * from "./node/validate.js"; // Include all shared exports -export * from "./schemas/index.js"; +export * from "./schemas/latest.js"; export * from "./shared/config.js"; export * from "./shared/constants.js"; +export * from "./types.js"; diff --git a/src/node/sign.ts b/src/node/sign.ts index bd4c8c4..bb83389 100644 --- a/src/node/sign.ts +++ b/src/node/sign.ts @@ -5,9 +5,8 @@ import forge from "node-forge"; import { tmpdir } from "os"; import { join } from "path"; import { promisify } from "util"; -import type { z } from "zod"; -import type { McpbSignatureInfoSchema } from "../shared/common.js"; +import type { McpbSignatureInfo } from "../types.js"; // Signature block markers const SIGNATURE_HEADER = "MCPB_SIG_V1"; @@ -89,20 +88,8 @@ export function signMcpbFile( // Create signature block with PKCS#7 data const signatureBlock = createSignatureBlock(pkcs7Signature); - // Update ZIP EOCD comment_length to include signature block - // This ensures strict ZIP parsers accept the signed file - const updatedContent = Buffer.from(mcpbContent); - const eocdOffset = findEocdOffset(updatedContent); - if (eocdOffset !== -1) { - const currentCommentLength = updatedContent.readUInt16LE(eocdOffset + 20); - updatedContent.writeUInt16LE( - currentCommentLength + signatureBlock.length, - eocdOffset + 20, - ); - } - // Append signature block to MCPB file - const signedContent = Buffer.concat([updatedContent, signatureBlock]); + const signedContent = Buffer.concat([mcpbContent, signatureBlock]); writeFileSync(mcpbPath, signedContent); } @@ -114,7 +101,7 @@ export function signMcpbFile( */ export async function verifyMcpbFile( mcpbPath: string, -): Promise> { +): Promise { try { const fileContent = readFileSync(mcpbPath); @@ -230,20 +217,6 @@ export async function verifyMcpbFile( } } -/** - * Finds the offset of the ZIP End of Central Directory record - * by scanning backwards for the EOCD magic bytes (0x06054b50) - */ -function findEocdOffset(buffer: Buffer): number { - // EOCD is at least 22 bytes, scan backwards from the end - for (let i = buffer.length - 22; i >= 0; i--) { - if (buffer.readUInt32LE(i) === 0x06054b50) { - return i; - } - } - return -1; -} - /** * Creates a signature block buffer with PKCS#7 signature */ diff --git a/src/node/validate.ts b/src/node/validate.ts index b958798..ac21747 100644 --- a/src/node/validate.ts +++ b/src/node/validate.ts @@ -2,16 +2,14 @@ import { existsSync, readFileSync, statSync } from "fs"; import * as fs from "fs/promises"; import { DestroyerOfModules } from "galactus"; import * as os from "os"; -import { dirname, extname, isAbsolute, join, resolve } from "path"; +import { dirname, isAbsolute, join, resolve } from "path"; import prettyBytes from "pretty-bytes"; import { unpackExtension } from "../cli/unpack.js"; import { - MANIFEST_SCHEMAS, - MANIFEST_SCHEMAS_LOOSE, + LATEST_MANIFEST_SCHEMA, + LATEST_MANIFEST_SCHEMA_LOOSE, } from "../shared/constants.js"; -import { getManifestVersionFromRawData } from "../shared/manifestVersionResolve.js"; -import { getAllFilesWithCount, readMcpbIgnorePatterns } from "./files.js"; /** * Check if a buffer contains a valid PNG file signature @@ -109,191 +107,7 @@ function validateIcon( }; } -interface ValidationResult { - valid: boolean; - errors: string[]; - warnings: string[]; -} - -// Expected file extensions by server type -const NODE_EXTENSIONS = new Set([".js", ".mjs", ".cjs"]); -const PYTHON_EXTENSIONS = new Set([".py"]); -const SCRIPT_EXTENSIONS = new Set([".js", ".mjs", ".cjs", ".py"]); - -/** - * Validate that the server entry_point file exists and matches the server type - */ -function validateEntryPoint( - manifest: { server: { type: string; entry_point: string } }, - baseDir: string, -): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - const { type, entry_point } = manifest.server; - const entryPath = join(baseDir, entry_point); - - if (!existsSync(entryPath)) { - errors.push(`Entry point file not found: ${entry_point}`); - return { valid: false, errors, warnings }; - } - - const ext = extname(entry_point).toLowerCase(); - - if (type === "node" && !NODE_EXTENSIONS.has(ext)) { - warnings.push( - `Unusual entry point extension "${ext}" for server type "node". Expected: .js, .mjs, or .cjs`, - ); - } else if ( - (type === "python" || type === "uv") && - !PYTHON_EXTENSIONS.has(ext) - ) { - warnings.push( - `Unusual entry point extension "${ext}" for server type "${type}". Expected: .py`, - ); - } else if (type === "binary" && SCRIPT_EXTENSIONS.has(ext)) { - warnings.push( - `Entry point has script extension "${ext}" but server type is "binary". Did you mean type "node" or "python"?`, - ); - } - - // For binary type on Unix, check executable bit - if (type === "binary" && process.platform !== "win32") { - const stat = statSync(entryPath); - if (!(stat.mode & 0o111)) { - errors.push( - `Binary entry point is not executable: ${entry_point}. Run: chmod +x ${entry_point}`, - ); - } - } - - return { valid: errors.length === 0, errors, warnings }; -} - -// Valid variable patterns from src/shared/config.ts replaceVariables() -const VALID_VARIABLE_PATTERN = - /^\$\{(__dirname|pathSeparator|\/|user_config\..+)\}$/; - -/** - * Validate that ${...} variables in mcp_config are recognized - */ -function validateCommandVariables(manifest: { - server: { - mcp_config: { - command?: string; - args?: string[]; - env?: Record; - platform_overrides?: Record< - string, - { - command?: string; - args?: string[]; - env?: Record; - } - >; - }; - }; -}): ValidationResult { - const errors: string[] = []; - const warnings: string[] = []; - - function checkString(value: string, context: string): void { - const variablePattern = /\$\{([^}]+)\}/g; - let match; - while ((match = variablePattern.exec(value)) !== null) { - const fullVar = match[0]; - if (!VALID_VARIABLE_PATTERN.test(fullVar)) { - errors.push( - `Invalid variable "${fullVar}" in ${context}. Valid variables: \${__dirname}, \${pathSeparator}, \${/}, \${user_config.}`, - ); - } - } - } - - function checkConfig( - config: { command?: string; args?: string[]; env?: Record }, - prefix: string, - ): void { - if (config.command) checkString(config.command, `${prefix}command`); - if (config.args) { - config.args.forEach((arg, i) => checkString(arg, `${prefix}args[${i}]`)); - } - if (config.env) { - for (const [key, val] of Object.entries(config.env)) { - checkString(val, `${prefix}env.${key}`); - } - } - } - - const { mcp_config } = manifest.server; - checkConfig(mcp_config, "mcp_config."); - - if (mcp_config.platform_overrides) { - for (const [platform, override] of Object.entries( - mcp_config.platform_overrides, - )) { - checkConfig(override, `mcp_config.platform_overrides.${platform}.`); - } - } - - return { valid: errors.length === 0, errors, warnings }; -} - -// Sensitive file patterns not already covered by EXCLUDE_PATTERNS in files.ts -const SENSITIVE_PATTERNS = [ - /(^|\/)credentials\.json$/i, - /(^|\/)secrets\./i, - /\.pem$/i, - /\.key$/i, - /\.p12$/i, - /\.pfx$/i, - /\.jks$/i, - /(^|\/)\.aws\//, - /(^|\/)\.ssh\//, - /(^|\/)id_rsa/, - /(^|\/)id_ed25519/, - /(^|\/)id_ecdsa/, - /\.keystore$/i, - /(^|\/)token\.json$/i, -]; - -/** - * Check if the file list that would be bundled contains sensitive files - */ -function validateSensitiveFiles(baseDir: string): ValidationResult { - const warnings: string[] = []; - - try { - const mcpbIgnorePatterns = readMcpbIgnorePatterns(baseDir); - const { files } = getAllFilesWithCount( - baseDir, - baseDir, - {}, - mcpbIgnorePatterns, - ); - - for (const filePath of Object.keys(files)) { - for (const pattern of SENSITIVE_PATTERNS) { - if (pattern.test(filePath)) { - warnings.push( - `Potentially sensitive file will be included in bundle: ${filePath}`, - ); - break; - } - } - } - } catch { - // If we can't read the directory, skip this check silently — - // pack will fail with a clearer error later - } - - // Sensitive files are always warnings, never errors — a .pem might be a legitimate TLS cert - return { valid: true, errors: [], warnings }; -} - -export function validateManifest( - inputPath: string, - options?: { projectDir?: string }, -): boolean { +export function validateManifest(inputPath: string): boolean { try { const resolvedPath = resolve(inputPath); let manifestPath = resolvedPath; @@ -305,34 +119,23 @@ export function validateManifest( const manifestContent = readFileSync(manifestPath, "utf-8"); const manifestData = JSON.parse(manifestContent); - const manifestVersion = getManifestVersionFromRawData(manifestData); - if (!manifestVersion) { - console.log("Unrecognized or unsupported manifest version"); - return false; - } - const result = MANIFEST_SCHEMAS[manifestVersion].safeParse(manifestData); + const result = LATEST_MANIFEST_SCHEMA.safeParse(manifestData); if (result.success) { console.log("Manifest schema validation passes!"); - const manifestDir = dirname(manifestPath); - // projectDir is where source files live — defaults to the manifest's directory - const projectDir = options?.projectDir - ? resolve(options.projectDir) - : manifestDir; - let hasErrors = false; - - // Validate icon if present (always relative to manifest directory) + // Validate icon if present if (manifestData.icon) { - const iconValidation = validateIcon(manifestData.icon, manifestDir); + const baseDir = dirname(manifestPath); + const iconValidation = validateIcon(manifestData.icon, baseDir); if (iconValidation.errors.length > 0) { console.log("\nERROR: Icon validation failed:\n"); iconValidation.errors.forEach((error) => { console.log(` - ${error}`); }); - hasErrors = true; + return false; } if (iconValidation.warnings.length > 0) { @@ -343,48 +146,7 @@ export function validateManifest( } } - // Validate entry point (relative to project directory) - const entryPointValidation = validateEntryPoint(manifestData, projectDir); - if (entryPointValidation.errors.length > 0) { - console.log("\nERROR: Entry point validation failed:\n"); - entryPointValidation.errors.forEach((error) => { - console.log(` - ${error}`); - }); - hasErrors = true; - } - if (entryPointValidation.warnings.length > 0) { - console.log("\nEntry point warnings:\n"); - entryPointValidation.warnings.forEach((warning) => { - console.log(` - ${warning}`); - }); - } - - // Validate command variables - const variableValidation = validateCommandVariables(manifestData); - if (variableValidation.errors.length > 0) { - console.log("\nERROR: Command variable validation failed:\n"); - variableValidation.errors.forEach((error) => { - console.log(` - ${error}`); - }); - hasErrors = true; - } - if (variableValidation.warnings.length > 0) { - console.log("\nCommand variable warnings:\n"); - variableValidation.warnings.forEach((warning) => { - console.log(` - ${warning}`); - }); - } - - // Check for sensitive files (relative to project directory) - const sensitiveValidation = validateSensitiveFiles(projectDir); - if (sensitiveValidation.warnings.length > 0) { - console.log("\nSensitive file warnings:\n"); - sensitiveValidation.warnings.forEach((warning) => { - console.log(` - ${warning}`); - }); - } - - return !hasErrors; + return true; } else { console.log("ERROR: Manifest validation failed:\n"); result.error.issues.forEach((issue) => { @@ -430,12 +192,7 @@ export async function cleanMcpb(inputPath: string) { const manifestPath = resolve(unpackPath, "manifest.json"); const originalManifest = await fs.readFile(manifestPath, "utf-8"); const manifestData = JSON.parse(originalManifest); - const manifestVersion = getManifestVersionFromRawData(manifestData); - if (!manifestVersion) { - throw new Error("Unrecognized or unsupported manifest version"); - } - const result = - MANIFEST_SCHEMAS_LOOSE[manifestVersion].safeParse(manifestData); + const result = LATEST_MANIFEST_SCHEMA_LOOSE.safeParse(manifestData); if (!result.success) { throw new Error( diff --git a/src/schemas/0.1.ts b/src/schemas/0.1.ts index 9922195..4d77ccc 100644 --- a/src/schemas/0.1.ts +++ b/src/schemas/0.1.ts @@ -71,6 +71,11 @@ export const McpbUserConfigurationOptionSchema = z.strictObject({ max: z.number().optional(), }); +export const McpbUserConfigValuesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), +); + export const McpbManifestSchema = z .strictObject({ $schema: z.string().optional(), @@ -102,8 +107,18 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), + _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), }) .refine((data) => !!(data.dxt_version || data.manifest_version), { message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); + +export const McpbSignatureInfoSchema = z.strictObject({ + status: z.enum(["signed", "unsigned", "self-signed"]), + publisher: z.string().optional(), + issuer: z.string().optional(), + valid_from: z.string().optional(), + valid_to: z.string().optional(), + fingerprint: z.string().optional(), +}); diff --git a/src/schemas/0.2.ts b/src/schemas/0.2.ts index bb1de16..f5d4d03 100644 --- a/src/schemas/0.2.ts +++ b/src/schemas/0.2.ts @@ -71,6 +71,11 @@ export const McpbUserConfigurationOptionSchema = z.strictObject({ max: z.number().optional(), }); +export const McpbUserConfigValuesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), +); + export const McpbManifestSchema = z .strictObject({ $schema: z.string().optional(), @@ -108,3 +113,12 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); + +export const McpbSignatureInfoSchema = z.strictObject({ + status: z.enum(["signed", "unsigned", "self-signed"]), + publisher: z.string().optional(), + issuer: z.string().optional(), + valid_from: z.string().optional(), + valid_to: z.string().optional(), + fingerprint: z.string().optional(), +}); diff --git a/src/schemas/0.3.ts b/src/schemas/0.3.ts index 2ca0206..2bdc946 100644 --- a/src/schemas/0.3.ts +++ b/src/schemas/0.3.ts @@ -76,6 +76,11 @@ export const McpbUserConfigurationOptionSchema = z.strictObject({ max: z.number().optional(), }); +export const McpbUserConfigValuesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), +); + export const McpbManifestLocalizationSchema = z.strictObject({ resources: z .string() @@ -142,3 +147,12 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); + +export const McpbSignatureInfoSchema = z.strictObject({ + status: z.enum(["signed", "unsigned", "self-signed"]), + publisher: z.string().optional(), + issuer: z.string().optional(), + valid_from: z.string().optional(), + valid_to: z.string().optional(), + fingerprint: z.string().optional(), +}); diff --git a/src/schemas/0.4.ts b/src/schemas/0.4.ts deleted file mode 100644 index ae97d17..0000000 --- a/src/schemas/0.4.ts +++ /dev/null @@ -1,144 +0,0 @@ -// v0.4: Adds UV runtime support for Python extensions -import * as z from "zod"; - -export const MANIFEST_VERSION = "0.4"; - -const LOCALE_PLACEHOLDER_REGEX = /\$\{locale\}/i; -const BCP47_REGEX = /^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$/; -const ICON_SIZE_REGEX = /^\d+x\d+$/; - -export const McpServerConfigSchema = z.strictObject({ - command: z.string(), - args: z.array(z.string()).optional(), - env: z.record(z.string(), z.string()).optional(), -}); - -export const McpbManifestAuthorSchema = z.strictObject({ - name: z.string(), - email: z.string().email().optional(), - url: z.string().url().optional(), -}); - -export const McpbManifestRepositorySchema = z.strictObject({ - type: z.string(), - url: z.string().url(), -}); - -export const McpbManifestPlatformOverrideSchema = - McpServerConfigSchema.partial(); - -export const McpbManifestMcpConfigSchema = McpServerConfigSchema.extend({ - platform_overrides: z - .record(z.string(), McpbManifestPlatformOverrideSchema) - .optional(), -}); - -export const McpbManifestServerSchema = z.strictObject({ - type: z.enum(["python", "node", "binary", "uv"]), - entry_point: z.string(), - mcp_config: McpbManifestMcpConfigSchema, -}); - -export const McpbManifestCompatibilitySchema = z.strictObject({ - claude_desktop: z.string().optional(), - platforms: z.array(z.enum(["darwin", "win32", "linux"])).optional(), - runtimes: z - .strictObject({ - python: z.string().optional(), - node: z.string().optional(), - }) - .optional(), -}); - -export const McpbManifestToolSchema = z.strictObject({ - name: z.string(), - description: z.string().optional(), -}); - -export const McpbManifestPromptSchema = z.strictObject({ - name: z.string(), - description: z.string().optional(), - arguments: z.array(z.string()).optional(), - text: z.string(), -}); - -export const McpbUserConfigurationOptionSchema = z.strictObject({ - type: z.enum(["string", "number", "boolean", "directory", "file"]), - title: z.string(), - description: z.string(), - required: z.boolean().optional(), - default: z - .union([z.string(), z.number(), z.boolean(), z.array(z.string())]) - .optional(), - multiple: z.boolean().optional(), - sensitive: z.boolean().optional(), - min: z.number().optional(), - max: z.number().optional(), -}); - -export const McpbManifestLocalizationSchema = z.strictObject({ - resources: z - .string() - .regex( - LOCALE_PLACEHOLDER_REGEX, - 'resources must include a "${locale}" placeholder', - ), - default_locale: z - .string() - .regex( - BCP47_REGEX, - "default_locale must be a valid BCP 47 locale identifier", - ), -}); - -export const McpbManifestIconSchema = z.strictObject({ - src: z.string(), - size: z - .string() - .regex( - ICON_SIZE_REGEX, - 'size must be in the format "WIDTHxHEIGHT" (e.g., "16x16")', - ), - theme: z.string().min(1, "theme cannot be empty when provided").optional(), -}); - -export const McpbManifestSchema = z - .strictObject({ - $schema: z.string().optional(), - dxt_version: z - .literal(MANIFEST_VERSION) - .optional() - .describe("@deprecated Use manifest_version instead"), - manifest_version: z.literal(MANIFEST_VERSION).optional(), - name: z.string(), - display_name: z.string().optional(), - version: z.string(), - description: z.string(), - long_description: z.string().optional(), - author: McpbManifestAuthorSchema, - repository: McpbManifestRepositorySchema.optional(), - homepage: z.string().url().optional(), - documentation: z.string().url().optional(), - support: z.string().url().optional(), - icon: z.string().optional(), - icons: z.array(McpbManifestIconSchema).optional(), - screenshots: z.array(z.string()).optional(), - localization: McpbManifestLocalizationSchema.optional(), - server: McpbManifestServerSchema, - tools: z.array(McpbManifestToolSchema).optional(), - tools_generated: z.boolean().optional(), - prompts: z.array(McpbManifestPromptSchema).optional(), - prompts_generated: z.boolean().optional(), - keywords: z.array(z.string()).optional(), - license: z.string().optional(), - privacy_policies: z.array(z.string().url()).optional(), - compatibility: McpbManifestCompatibilitySchema.optional(), - user_config: z - .record(z.string(), McpbUserConfigurationOptionSchema) - .optional(), - _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), - }) - .refine((data) => !!(data.dxt_version || data.manifest_version), { - message: - "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", - }); diff --git a/src/schemas/any.ts b/src/schemas/any.ts index 1b374c7..bd7cec8 100644 --- a/src/schemas/any.ts +++ b/src/schemas/any.ts @@ -3,15 +3,13 @@ import * as z from "zod"; import * as v0_1 from "./0.1.js"; import * as v0_2 from "./0.2.js"; import * as v0_3 from "./0.3.js"; -import * as v0_4 from "./0.4.js"; /** - * Union schema that accepts any supported manifest version. + * Union schema that accepts any supported manifest version (0.1, 0.2, 0.3). * Use this when you need to validate manifests of any version. */ export const McpbManifestSchema = z.union([ v0_1.McpbManifestSchema, v0_2.McpbManifestSchema, v0_3.McpbManifestSchema, - v0_4.McpbManifestSchema, ]); diff --git a/src/schemas/index.ts b/src/schemas/index.ts index 6d1405b..3729f0e 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -1,20 +1,9 @@ -import { McpbManifestSchema as ManifestSchemaV0_1 } from "./0.1.js"; -import { McpbManifestSchema as ManifestSchemaV0_2 } from "./0.2.js"; -import { McpbManifestSchema as ManifestSchemaV0_3 } from "./0.3.js"; -import { McpbManifestSchema as ManifestSchemaV0_4 } from "./0.4.js"; - export * as v0_1 from "./0.1.js"; export * as v0_2 from "./0.2.js"; export * as v0_3 from "./0.3.js"; -export * as v0_4 from "./0.4.js"; -export * as vAny from "./any.js"; - -/** - * Map of manifest versions to their strict schemas - */ -export const VERSIONED_MANIFEST_SCHEMAS = { - "0.1": ManifestSchemaV0_1, - "0.2": ManifestSchemaV0_2, - "0.3": ManifestSchemaV0_3, - "0.4": ManifestSchemaV0_4, -} as const; +export * as any from "./any.js"; +export * as latest from "./latest.js"; +export { + MANIFEST_VERSION as LATEST_MANIFEST_VERSION, + McpbManifestSchema, +} from "./latest.js"; diff --git a/src/schemas/latest.ts b/src/schemas/latest.ts new file mode 100644 index 0000000..e79a613 --- /dev/null +++ b/src/schemas/latest.ts @@ -0,0 +1 @@ +export * from "./0.3.js"; diff --git a/src/schemas_loose/0.1.ts b/src/schemas_loose/0.1.ts index ab7f69c..2ab5e56 100644 --- a/src/schemas_loose/0.1.ts +++ b/src/schemas_loose/0.1.ts @@ -73,6 +73,11 @@ export const McpbUserConfigurationOptionSchema = z.object({ max: z.number().optional(), }); +export const McpbUserConfigValuesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), +); + export const McpbManifestSchema = z .object({ $schema: z.string().optional(), @@ -104,8 +109,18 @@ export const McpbManifestSchema = z user_config: z .record(z.string(), McpbUserConfigurationOptionSchema) .optional(), + _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), }) .refine((data) => !!(data.dxt_version || data.manifest_version), { message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); + +export const McpbSignatureInfoSchema = z.object({ + status: z.enum(["signed", "unsigned", "self-signed"]), + publisher: z.string().optional(), + issuer: z.string().optional(), + valid_from: z.string().optional(), + valid_to: z.string().optional(), + fingerprint: z.string().optional(), +}); diff --git a/src/schemas_loose/0.2.ts b/src/schemas_loose/0.2.ts index bc177d2..7cdbc1a 100644 --- a/src/schemas_loose/0.2.ts +++ b/src/schemas_loose/0.2.ts @@ -73,6 +73,11 @@ export const McpbUserConfigurationOptionSchema = z.object({ max: z.number().optional(), }); +export const McpbUserConfigValuesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), +); + export const McpbManifestSchema = z .object({ $schema: z.string().optional(), @@ -111,3 +116,12 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); + +export const McpbSignatureInfoSchema = z.object({ + status: z.enum(["signed", "unsigned", "self-signed"]), + publisher: z.string().optional(), + issuer: z.string().optional(), + valid_from: z.string().optional(), + valid_to: z.string().optional(), + fingerprint: z.string().optional(), +}); diff --git a/src/schemas_loose/0.3.ts b/src/schemas_loose/0.3.ts index a96894b..87ac125 100644 --- a/src/schemas_loose/0.3.ts +++ b/src/schemas_loose/0.3.ts @@ -77,6 +77,11 @@ export const McpbUserConfigurationOptionSchema = z.object({ max: z.number().optional(), }); +export const McpbUserConfigValuesSchema = z.record( + z.string(), + z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), +); + export const McpbManifestLocalizationSchema = z .object({ resources: z @@ -148,3 +153,12 @@ export const McpbManifestSchema = z message: "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", }); + +export const McpbSignatureInfoSchema = z.object({ + status: z.enum(["signed", "unsigned", "self-signed"]), + publisher: z.string().optional(), + issuer: z.string().optional(), + valid_from: z.string().optional(), + valid_to: z.string().optional(), + fingerprint: z.string().optional(), +}); diff --git a/src/schemas_loose/0.4.ts b/src/schemas_loose/0.4.ts deleted file mode 100644 index 395fd60..0000000 --- a/src/schemas_loose/0.4.ts +++ /dev/null @@ -1,152 +0,0 @@ -// v0.4: Adds UV runtime support for Python extensions -import * as z from "zod"; - -export const MANIFEST_VERSION = "0.4"; - -const LOCALE_PLACEHOLDER_REGEX = /\$\{locale\}/i; -const BCP47_REGEX = /^[A-Za-z0-9]{2,8}(?:-[A-Za-z0-9]{1,8})*$/; -const ICON_SIZE_REGEX = /^\d+x\d+$/; - -export const McpServerConfigSchema = z.object({ - command: z.string(), - args: z.array(z.string()).optional(), - env: z.record(z.string(), z.string()).optional(), -}); - -export const McpbManifestAuthorSchema = z.object({ - name: z.string(), - email: z.string().email().optional(), - url: z.string().url().optional(), -}); - -export const McpbManifestRepositorySchema = z.object({ - type: z.string(), - url: z.string().url(), -}); - -export const McpbManifestPlatformOverrideSchema = - McpServerConfigSchema.partial(); - -export const McpbManifestMcpConfigSchema = McpServerConfigSchema.extend({ - platform_overrides: z - .record(z.string(), McpbManifestPlatformOverrideSchema) - .optional(), -}); - -export const McpbManifestServerSchema = z.object({ - type: z.enum(["python", "node", "binary", "uv"]), - entry_point: z.string(), - // mcp_config is optional for UV type (UV handles execution) - mcp_config: McpbManifestMcpConfigSchema.optional(), -}); - -export const McpbManifestCompatibilitySchema = z - .object({ - claude_desktop: z.string().optional(), - platforms: z.array(z.enum(["darwin", "win32", "linux"])).optional(), - runtimes: z - .object({ - python: z.string().optional(), - node: z.string().optional(), - }) - .optional(), - }) - .passthrough(); - -export const McpbManifestToolSchema = z.object({ - name: z.string(), - description: z.string().optional(), -}); - -export const McpbManifestPromptSchema = z.object({ - name: z.string(), - description: z.string().optional(), - arguments: z.array(z.string()).optional(), - text: z.string(), -}); - -export const McpbUserConfigurationOptionSchema = z.object({ - type: z.enum(["string", "number", "boolean", "directory", "file"]), - title: z.string(), - description: z.string(), - required: z.boolean().optional(), - default: z - .union([z.string(), z.number(), z.boolean(), z.array(z.string())]) - .optional(), - multiple: z.boolean().optional(), - sensitive: z.boolean().optional(), - min: z.number().optional(), - max: z.number().optional(), -}); - -export const McpbManifestLocalizationSchema = z - .object({ - resources: z - .string() - .regex( - LOCALE_PLACEHOLDER_REGEX, - 'resources must include a "${locale}" placeholder', - ), - default_locale: z - .string() - .regex( - BCP47_REGEX, - "default_locale must be a valid BCP 47 locale identifier", - ), - }) - .passthrough(); - -export const McpbManifestIconSchema = z - .object({ - src: z.string(), - size: z - .string() - .regex( - ICON_SIZE_REGEX, - 'size must be in the format "WIDTHxHEIGHT" (e.g., "16x16")', - ), - theme: z.string().min(1).optional(), - }) - .passthrough(); - -export const McpbManifestSchema = z - .object({ - $schema: z.string().optional(), - dxt_version: z - .literal(MANIFEST_VERSION) - .optional() - .describe("@deprecated Use manifest_version instead"), - manifest_version: z.literal(MANIFEST_VERSION).optional(), - name: z.string(), - display_name: z.string().optional(), - version: z.string(), - description: z.string(), - long_description: z.string().optional(), - author: McpbManifestAuthorSchema, - repository: McpbManifestRepositorySchema.optional(), - homepage: z.string().url().optional(), - documentation: z.string().url().optional(), - support: z.string().url().optional(), - icon: z.string().optional(), - icons: z.array(McpbManifestIconSchema).optional(), - screenshots: z.array(z.string()).optional(), - localization: McpbManifestLocalizationSchema.optional(), - server: McpbManifestServerSchema, - tools: z.array(McpbManifestToolSchema).optional(), - tools_generated: z.boolean().optional(), - prompts: z.array(McpbManifestPromptSchema).optional(), - prompts_generated: z.boolean().optional(), - keywords: z.array(z.string()).optional(), - license: z.string().optional(), - privacy_policies: z.array(z.string().url()).optional(), - compatibility: McpbManifestCompatibilitySchema.optional(), - user_config: z - .record(z.string(), McpbUserConfigurationOptionSchema) - .optional(), - _meta: z.record(z.string(), z.record(z.string(), z.any())).optional(), - }) - .passthrough() - .refine((data) => !!(data.dxt_version || data.manifest_version), { - message: - "Either 'dxt_version' (deprecated) or 'manifest_version' must be provided", - }); diff --git a/src/schemas_loose/index.ts b/src/schemas_loose/index.ts index c2f1a01..3548b1d 100644 --- a/src/schemas_loose/index.ts +++ b/src/schemas_loose/index.ts @@ -1,4 +1,4 @@ export * as v0_1 from "./0.1.js"; export * as v0_2 from "./0.2.js"; export * as v0_3 from "./0.3.js"; -export * as v0_4 from "./0.4.js"; +export * as latest from "./latest.js"; diff --git a/src/schemas_loose/latest.ts b/src/schemas_loose/latest.ts new file mode 100644 index 0000000..e79a613 --- /dev/null +++ b/src/schemas_loose/latest.ts @@ -0,0 +1 @@ +export * from "./0.3.js"; diff --git a/src/shared/common.ts b/src/shared/common.ts deleted file mode 100644 index 9a8aeb3..0000000 --- a/src/shared/common.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { z } from "zod"; - -export const McpbUserConfigValuesSchema = z.record( - z.string(), - z.union([z.string(), z.number(), z.boolean(), z.array(z.string())]), -); - -export const McpbSignatureInfoSchema = z.strictObject({ - status: z.enum(["signed", "unsigned", "self-signed"]), - publisher: z.string().optional(), - issuer: z.string().optional(), - valid_from: z.string().optional(), - valid_to: z.string().optional(), - fingerprint: z.string().optional(), -}); - -export type McpbSignatureInfo = z.infer; -export type McpbUserConfigValues = z.infer; diff --git a/src/shared/config.ts b/src/shared/config.ts index 7073f58..20e4262 100644 --- a/src/shared/config.ts +++ b/src/shared/config.ts @@ -1,7 +1,9 @@ -import type { z } from "zod"; - -import type { Logger, McpbManifestAny } from "../types.js"; -import type { McpbUserConfigValuesSchema } from "./common.js"; +import type { + Logger, + McpbManifest, + McpbUserConfigValues, + McpServerConfig, +} from "../types.js"; /** * This file contains utility functions for handling MCPB configuration, @@ -83,17 +85,17 @@ export function replaceVariables( } interface GetMcpConfigForManifestOptions { - manifest: McpbManifestAny; + manifest: McpbManifest; extensionPath: string; systemDirs: Record; - userConfig: z.infer; + userConfig: McpbUserConfigValues; pathSeparator: string; logger?: Logger; } export async function getMcpConfigForManifest( options: GetMcpConfigForManifestOptions, -): Promise { +): Promise { const { manifest, extensionPath, @@ -107,7 +109,7 @@ export async function getMcpConfigForManifest( return undefined; } - let result: McpbManifestAny["server"]["mcp_config"] = { + let result: McpServerConfig = { ...baseConfig, }; @@ -117,9 +119,7 @@ export async function getMcpConfigForManifest( result.command = platformConfig.command || result.command; result.args = platformConfig.args || result.args; - result.env = platformConfig.env - ? { ...result.env, ...platformConfig.env } - : result.env; + result.env = platformConfig.env || result.env; } } @@ -173,17 +173,14 @@ export async function getMcpConfigForManifest( } // Replace all variables in the config - result = replaceVariables( - result, - variables, - ) as McpbManifestAny["server"]["mcp_config"]; + result = replaceVariables(result, variables) as McpServerConfig; return result; } interface HasRequiredConfigMissingOptions { - manifest: McpbManifestAny; - userConfig?: z.infer; + manifest: McpbManifest; + userConfig?: McpbUserConfigValues; } function isInvalidSingleValue(value: unknown): boolean { diff --git a/src/shared/constants.ts b/src/shared/constants.ts index a7c460a..95a5cf4 100644 --- a/src/shared/constants.ts +++ b/src/shared/constants.ts @@ -1,21 +1,16 @@ import { McpbManifestSchema as ManifestSchemaV0_1 } from "../schemas/0.1.js"; import { McpbManifestSchema as ManifestSchemaV0_2 } from "../schemas/0.2.js"; import { McpbManifestSchema as ManifestSchemaV0_3 } from "../schemas/0.3.js"; -import { McpbManifestSchema as ManifestSchemaV0_4 } from "../schemas/0.4.js"; +import { McpbManifestSchema as CurrentManifestSchema } from "../schemas/latest.js"; import { McpbManifestSchema as LooseManifestSchemaV0_1 } from "../schemas_loose/0.1.js"; import { McpbManifestSchema as LooseManifestSchemaV0_2 } from "../schemas_loose/0.2.js"; import { McpbManifestSchema as LooseManifestSchemaV0_3 } from "../schemas_loose/0.3.js"; -import { McpbManifestSchema as LooseManifestSchemaV0_4 } from "../schemas_loose/0.4.js"; +import { McpbManifestSchema as CurrentLooseManifestSchema } from "../schemas_loose/latest.js"; /** - * Latest manifest version - indicates the maximum supported version by vAny schema + * Latest manifest version - the version that new manifests should use */ -export const LATEST_MANIFEST_VERSION = "0.4" as const; - -/** - * Default manifest version for new packages - */ -export const DEFAULT_MANIFEST_VERSION = "0.3" as const; +export const LATEST_MANIFEST_VERSION = "0.3" as const; /** * Map of manifest versions to their strict schemas @@ -24,7 +19,6 @@ export const MANIFEST_SCHEMAS = { "0.1": ManifestSchemaV0_1, "0.2": ManifestSchemaV0_2, "0.3": ManifestSchemaV0_3, - "0.4": ManifestSchemaV0_4, } as const; /** @@ -34,5 +28,14 @@ export const MANIFEST_SCHEMAS_LOOSE = { "0.1": LooseManifestSchemaV0_1, "0.2": LooseManifestSchemaV0_2, "0.3": LooseManifestSchemaV0_3, - "0.4": LooseManifestSchemaV0_4, } as const; + +/** + * Get the latest manifest schema based on LATEST_MANIFEST_VERSION + */ +export const LATEST_MANIFEST_SCHEMA = CurrentManifestSchema; + +/** + * Get the latest loose manifest schema based on LATEST_MANIFEST_VERSION + */ +export const LATEST_MANIFEST_SCHEMA_LOOSE = CurrentLooseManifestSchema; diff --git a/src/shared/manifestVersionResolve.ts b/src/shared/manifestVersionResolve.ts deleted file mode 100644 index 5bc6381..0000000 --- a/src/shared/manifestVersionResolve.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { MANIFEST_SCHEMAS } from "./constants.js"; - -export function getManifestVersionFromRawData(manifestData: unknown) { - let manifestVersion: keyof typeof MANIFEST_SCHEMAS | null = null; - if ( - typeof manifestData === "object" && - manifestData && - "manifest_version" in manifestData && - typeof manifestData.manifest_version === "string" && - Object.keys(MANIFEST_SCHEMAS).includes(manifestData.manifest_version) - ) { - manifestVersion = - manifestData.manifest_version as keyof typeof MANIFEST_SCHEMAS; - } else if ( - typeof manifestData === "object" && - manifestData && - "dxt_version" in manifestData && - typeof manifestData.dxt_version === "string" && - Object.keys(MANIFEST_SCHEMAS).includes(manifestData.dxt_version) - ) { - manifestVersion = manifestData.dxt_version as keyof typeof MANIFEST_SCHEMAS; - } - return manifestVersion; -} diff --git a/src/types.ts b/src/types.ts index 655bffa..9fb510f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,11 +1,68 @@ import type * as z from "zod"; import type { McpbManifestSchema as McpbManifestSchemaAny } from "./schemas/any.js"; +import type { + McpbManifestAuthorSchema, + McpbManifestCompatibilitySchema, + McpbManifestMcpConfigSchema, + McpbManifestPlatformOverrideSchema, + McpbManifestPromptSchema, + McpbManifestRepositorySchema, + McpbManifestSchema, + McpbManifestServerSchema, + McpbManifestToolSchema, + McpbSignatureInfoSchema, + McpbUserConfigurationOptionSchema, + McpbUserConfigValuesSchema, + McpServerConfigSchema, +} from "./schemas/latest.js"; + +export type McpServerConfig = z.infer; + +export type McpbManifestAuthor = z.infer; + +export type McpbManifestRepository = z.infer< + typeof McpbManifestRepositorySchema +>; + +export type McpbManifestPlatformOverride = z.infer< + typeof McpbManifestPlatformOverrideSchema +>; + +export type McpbManifestMcpConfig = z.infer; + +export type McpbManifestServer = z.infer; + +export type McpbManifestCompatibility = z.infer< + typeof McpbManifestCompatibilitySchema +>; + +export type McpbManifestTool = z.infer; + +export type McpbManifestPrompt = z.infer; + +export type McpbUserConfigurationOption = z.infer< + typeof McpbUserConfigurationOptionSchema +>; + +export type McpbUserConfigValues = z.infer; /** - * McpbManifest type representing the union of all manifest versions + * McpbManifest type that accepts any supported manifest version + * This is the default manifest type that should be used for maximum compatibility. */ -export type McpbManifestAny = z.infer; +export type McpbManifest = z.infer; + +/** + * McpbManifest type for the latest manifest version only + * Use this when you specifically need the latest version. + */ +export type McpbManifestLatest = z.infer; + +/** + * Information about a MCPB package signature + */ +export type McpbSignatureInfo = z.infer; export interface Logger { log: (...args: unknown[]) => void; diff --git a/test/cli.test.ts b/test/cli.test.ts index b74e9ac..9bdc1eb 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -2,7 +2,7 @@ import { execSync } from "node:child_process"; import fs from "node:fs"; import { join } from "node:path"; -import { DEFAULT_MANIFEST_VERSION } from "../src/shared/constants"; +import { LATEST_MANIFEST_VERSION } from "../src/schemas/index.js"; interface ExecSyncError extends Error { stdout: Buffer; @@ -99,10 +99,6 @@ describe("DXT CLI", () => { 0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82, ]); fs.writeFileSync(join(testDir, "icon.png"), validPngBuffer); - - // Create entry point file referenced by test manifests - fs.mkdirSync(join(testDir, "server"), { recursive: true }); - fs.writeFileSync(join(testDir, "server", "index.js"), "// fixture"); }); afterAll(() => { @@ -117,7 +113,7 @@ describe("DXT CLI", () => { fs.writeFileSync( manifestWithIcon, JSON.stringify({ - manifest_version: DEFAULT_MANIFEST_VERSION, + manifest_version: LATEST_MANIFEST_VERSION, name: "test-with-icon", version: "1.0.0", description: "Test with icon", @@ -144,7 +140,7 @@ describe("DXT CLI", () => { fs.writeFileSync( manifestWithUrl, JSON.stringify({ - manifest_version: DEFAULT_MANIFEST_VERSION, + manifest_version: LATEST_MANIFEST_VERSION, name: "test-with-url", version: "1.0.0", description: "Test with URL", @@ -181,7 +177,7 @@ describe("DXT CLI", () => { fs.writeFileSync( join(tempDir, "manifest.json"), JSON.stringify({ - manifest_version: DEFAULT_MANIFEST_VERSION, + manifest_version: LATEST_MANIFEST_VERSION, name: "Test Extension", version: "1.0.0", description: "A test extension", @@ -200,9 +196,6 @@ describe("DXT CLI", () => { fs.writeFileSync(join(tempDir, "file1.txt"), "hello"); fs.mkdirSync(join(tempDir, "subdir")); fs.writeFileSync(join(tempDir, "subdir", "file2.txt"), "world"); - // Create entry point file referenced by manifest - fs.mkdirSync(join(tempDir, "server"), { recursive: true }); - fs.writeFileSync(join(tempDir, "server", "index.js"), "// fixture"); }); afterAll(() => { @@ -265,84 +258,6 @@ describe("DXT CLI", () => { expect(originalFile2).toEqual(unpackedFile2); }); - it("should pack with --manifest pointing to a separate directory", () => { - const projectDir = join(__dirname, "temp-manifest-project"); - const manifestDir = join(__dirname, "temp-manifest-separate"); - const manifestPackedPath = join(__dirname, "test-manifest-flag.mcpb"); - - try { - // Create project directory with source files (no manifest) - fs.mkdirSync(join(projectDir, "server"), { recursive: true }); - fs.writeFileSync( - join(projectDir, "server", "index.js"), - "console.log('hello');", - ); - - // Create separate manifest directory - fs.mkdirSync(manifestDir, { recursive: true }); - fs.writeFileSync( - join(manifestDir, "manifest.json"), - JSON.stringify({ - manifest_version: DEFAULT_MANIFEST_VERSION, - name: "Test Separate Manifest", - version: "1.0.0", - description: "Test with separate manifest", - author: { name: "MCPB" }, - server: { - type: "node", - entry_point: "server/index.js", - mcp_config: { command: "node" }, - }, - }), - ); - - const result = execSync( - `node ${cliPath} pack ${projectDir} ${manifestPackedPath} --manifest ${join(manifestDir, "manifest.json")}`, - { encoding: "utf-8" }, - ); - - expect(fs.existsSync(manifestPackedPath)).toBe(true); - expect(result).toContain("Validating manifest"); - } finally { - fs.rmSync(projectDir, { recursive: true, force: true }); - fs.rmSync(manifestDir, { recursive: true, force: true }); - if (fs.existsSync(manifestPackedPath)) { - fs.unlinkSync(manifestPackedPath); - } - } - }); - - it("should fail with --manifest pointing to nonexistent file", () => { - const projectDir = join(__dirname, "temp-manifest-missing-project"); - - try { - fs.mkdirSync(projectDir, { recursive: true }); - fs.writeFileSync(join(projectDir, "index.js"), "console.log('hello');"); - - expect(() => { - execSync( - `node ${cliPath} pack ${projectDir} /tmp/out.mcpb --manifest /nonexistent/manifest.json`, - { encoding: "utf-8", stdio: "pipe" }, - ); - }).toThrow(); - - try { - execSync( - `node ${cliPath} pack ${projectDir} /tmp/out.mcpb --manifest /nonexistent/manifest.json`, - { encoding: "utf-8", stdio: "pipe" }, - ); - } catch (error: unknown) { - const execError = error as { stdout?: Buffer; stderr?: Buffer }; - const output = - (execError.stdout?.toString() || "") + - (execError.stderr?.toString() || ""); - expect(output).toContain("Manifest file not found"); - } - } finally { - fs.rmSync(projectDir, { recursive: true, force: true }); - } - }); - it("should preserve executable file permissions after packing and unpacking", () => { // Skip this test on Windows since it doesn't support Unix permissions if (process.platform === "win32") { @@ -359,7 +274,7 @@ describe("DXT CLI", () => { fs.writeFileSync( join(tempExecDir, "manifest.json"), JSON.stringify({ - manifest_version: DEFAULT_MANIFEST_VERSION, + manifest_version: LATEST_MANIFEST_VERSION, name: "Test Executable Extension", version: "1.0.0", description: "A test extension with executable files", @@ -376,10 +291,6 @@ describe("DXT CLI", () => { }), ); - // Create entry point file referenced by manifest - fs.mkdirSync(join(tempExecDir, "server"), { recursive: true }); - fs.writeFileSync(join(tempExecDir, "server", "index.js"), "// fixture"); - // Create an executable script const executableScript = join(tempExecDir, "run-script.sh"); fs.writeFileSync( @@ -435,82 +346,5 @@ describe("DXT CLI", () => { } } }); - - it("should add group+other execute bits on binary entry points during pack", () => { - // Skip this test on Windows since it doesn't support Unix permissions - if (process.platform === "win32") { - return; - } - - const tempBinDir = join(__dirname, "temp-binary-entry-test"); - const binPackedFilePath = join(__dirname, "test-binary-entry.dxt"); - const binUnpackedDir = join(__dirname, "temp-binary-entry-unpack"); - - try { - // Create a binary-type extension with owner-only execute (0o700) - // This passes validation (has some +x) but pack should add - // group+other execute bits for compatibility with CD extraction - fs.mkdirSync(join(tempBinDir, "server"), { recursive: true }); - fs.writeFileSync( - join(tempBinDir, "manifest.json"), - JSON.stringify({ - manifest_version: DEFAULT_MANIFEST_VERSION, - name: "Test Binary Extension", - version: "1.0.0", - description: "A test extension with binary entry point", - author: { - name: "MCPB", - }, - server: { - type: "binary", - entry_point: "server/myserver", - mcp_config: { - command: "${__dirname}/server/myserver", - }, - }, - }), - ); - - // Create binary entry point with 700 (owner-only execute) - fs.writeFileSync( - join(tempBinDir, "server", "myserver"), - "#!/bin/sh\necho binary", - ); - fs.chmodSync(join(tempBinDir, "server", "myserver"), 0o700); - - // Create a regular file for comparison (should stay 644) - fs.writeFileSync(join(tempBinDir, "config.json"), "{}"); - fs.chmodSync(join(tempBinDir, "config.json"), 0o644); - - // Pack the extension - execSync(`node ${cliPath} pack ${tempBinDir} ${binPackedFilePath}`, { - encoding: "utf-8", - }); - - // Unpack to check stored permissions - execSync( - `node ${cliPath} unpack ${binPackedFilePath} ${binUnpackedDir}`, - { - encoding: "utf-8", - }, - ); - - // Binary entry point should have full execute bits (700 | 111 = 711) - const entryStats = fs.statSync( - join(binUnpackedDir, "server", "myserver"), - ); - expect(entryStats.mode & 0o777).toBe(0o711); - - // Non-entry-point file should retain original permissions - const configStats = fs.statSync(join(binUnpackedDir, "config.json")); - expect(configStats.mode & 0o777).toBe(0o644); - } finally { - fs.rmSync(tempBinDir, { recursive: true, force: true }); - fs.rmSync(binUnpackedDir, { recursive: true, force: true }); - if (fs.existsSync(binPackedFilePath)) { - fs.unlinkSync(binPackedFilePath); - } - } - }); }); }); diff --git a/test/config.test.ts b/test/config.test.ts index 7e29f69..f7a0eb8 100644 --- a/test/config.test.ts +++ b/test/config.test.ts @@ -3,7 +3,7 @@ import { hasRequiredConfigMissing, replaceVariables, } from "../src/shared/config"; -import type { Logger, McpbManifestAny } from "../src/types"; +import type { Logger, McpbManifest } from "../src/types"; describe("replaceVariables", () => { it("should replace variables in strings", () => { @@ -89,8 +89,8 @@ describe("getMcpConfigForManifest", () => { data: "/data", }; - const baseManifest: McpbManifestAny = { - manifest_version: "0.2", + const baseManifest: McpbManifest = { + manifest_version: "0.3", name: "test-extension", version: "1.0.0", description: "Test extension", @@ -114,7 +114,7 @@ describe("getMcpConfigForManifest", () => { const manifest = { ...baseManifest, server: undefined, - } as unknown as McpbManifestAny; + } as unknown as McpbManifest; const result = await getMcpConfigForManifest({ manifest, @@ -129,7 +129,7 @@ describe("getMcpConfigForManifest", () => { }); it("should return undefined when required config is missing", async () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { apiKey: { @@ -171,7 +171,7 @@ describe("getMcpConfigForManifest", () => { }); it("should apply platform overrides", async () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, server: { type: "node", @@ -203,7 +203,7 @@ describe("getMcpConfigForManifest", () => { }); it("should handle user config variable replacement with defaults", async () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { port: { @@ -236,7 +236,7 @@ describe("getMcpConfigForManifest", () => { }); it("should handle user config variable replacement with user values", async () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { paths: { @@ -269,56 +269,8 @@ describe("getMcpConfigForManifest", () => { expect(result?.args).toEqual(["server.js", "/user/path1", "/user/path2"]); }); - it("should merge platform override env with base env instead of replacing", async () => { - const manifest: McpbManifestAny = { - ...baseManifest, - user_config: { - data_directory: { - type: "directory", - title: "Data Directory", - description: "Where data lives", - required: true, - }, - }, - server: { - type: "node", - entry_point: "server.js", - mcp_config: { - command: "node", - args: ["server.js"], - env: { - DATA_DIR: "${user_config.data_directory}", - EXISTING_VAR: "keep-me", - }, - platform_overrides: { - [process.platform]: { - env: { - PATH: "/usr/local/bin:${HOME}/.local/bin", - }, - }, - }, - }, - }, - }; - - const result = await getMcpConfigForManifest({ - manifest, - extensionPath: "/ext/path", - systemDirs: { ...mockSystemDirs, HOME: "/home/user" }, - userConfig: { data_directory: "/custom/data" }, - pathSeparator: "/", - logger: mockLogger, - }); - - // Base env vars should be preserved after platform override merge - expect(result?.env?.DATA_DIR).toBe("/custom/data"); - expect(result?.env?.EXISTING_VAR).toBe("keep-me"); - // Platform override env should also be present - expect(result?.env?.PATH).toBe("/usr/local/bin:/home/user/.local/bin"); - }); - it("should convert boolean user config values to strings", async () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { verbose: { @@ -352,8 +304,8 @@ describe("getMcpConfigForManifest", () => { }); describe("hasRequiredConfigMissing", () => { - const baseManifest: McpbManifestAny = { - manifest_version: "0.2", + const baseManifest: McpbManifest = { + manifest_version: "0.3", name: "test-extension", version: "1.0.0", description: "Test extension", @@ -377,7 +329,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return false when no config fields are required", () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { port: { @@ -397,7 +349,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return false when required config is provided", () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { apiKey: { @@ -417,7 +369,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return true when required config is undefined", () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { apiKey: { @@ -437,7 +389,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return true when required config is empty string", () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { apiKey: { @@ -457,7 +409,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return true when required config is array with invalid values", () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { paths: { @@ -478,7 +430,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should return true when required config is empty array", () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { paths: { @@ -507,7 +459,7 @@ describe("hasRequiredConfigMissing", () => { }); it("should handle multiple required config fields", () => { - const manifest: McpbManifestAny = { + const manifest: McpbManifest = { ...baseManifest, user_config: { apiKey: { diff --git a/test/icon-validation.test.ts b/test/icon-validation.test.ts index bdc7bbe..b55abd4 100644 --- a/test/icon-validation.test.ts +++ b/test/icon-validation.test.ts @@ -93,10 +93,6 @@ describe("Icon Validation", () => { "Not a PNG file", ); - // Create entry point file referenced by test manifests - fs.mkdirSync(join(testFixturesDir, "server"), { recursive: true }); - fs.writeFileSync(join(testFixturesDir, "server", "index.js"), "// fixture"); - // Create test manifests createTestManifest("valid-local-icon.json", { icon: "valid-icon.png", diff --git a/test/init.test.ts b/test/init.test.ts index 7acd57a..caf598f 100644 --- a/test/init.test.ts +++ b/test/init.test.ts @@ -10,7 +10,7 @@ import { getDefaultRepositoryUrl, readPackageJson, } from "../src/cli/init.js"; -import { DEFAULT_MANIFEST_VERSION } from "../src/shared/constants.js"; +import { LATEST_MANIFEST_VERSION } from "../src/schemas/index.js"; // Mock the fs module jest.mock("fs", () => ({ @@ -216,11 +216,11 @@ describe("init functions", () => { keywords: "", license: "", }, - // undefined, // localization + undefined, ); expect(manifest).toEqual({ - manifest_version: DEFAULT_MANIFEST_VERSION, + manifest_version: LATEST_MANIFEST_VERSION, name: "test-extension", version: "1.0.0", description: "Test description", @@ -315,14 +315,14 @@ describe("init functions", () => { license: "MIT", repository: { type: "git", url: "https://github.com/user/repo" }, }, - // { // localization - // resources: "resources/${locale}.json", - // default_locale: "en-US", - // }, + { + resources: "resources/${locale}.json", + default_locale: "en-US", + }, ); expect(manifest).toEqual({ - manifest_version: DEFAULT_MANIFEST_VERSION, + manifest_version: LATEST_MANIFEST_VERSION, name: "test-extension", display_name: "Test Extension", version: "1.0.0", @@ -351,6 +351,10 @@ describe("init functions", () => { }, ], screenshots: ["screen1.png", "screen2.png"], + localization: { + resources: "resources/${locale}.json", + default_locale: "en-US", + }, server: { type: "python", entry_point: "server/main.py", diff --git a/test/schemas.test.ts b/test/schemas.test.ts index 52943e0..70f181f 100644 --- a/test/schemas.test.ts +++ b/test/schemas.test.ts @@ -1,7 +1,7 @@ import { readFileSync } from "fs"; import { join } from "path"; -import { v0_2, v0_3 } from "../src/schemas/index.js"; +import { McpbManifestSchema, v0_2, v0_3 } from "../src/schemas/index.js"; describe("McpbManifestSchema", () => { it("should validate a valid manifest", () => { @@ -9,7 +9,7 @@ describe("McpbManifestSchema", () => { const manifestContent = readFileSync(manifestPath, "utf-8"); const manifestData = JSON.parse(manifestContent); - const result = v0_3.McpbManifestSchema.safeParse(manifestData); + const result = McpbManifestSchema.safeParse(manifestData); expect(result.success).toBe(true); if (result.success) { @@ -23,7 +23,7 @@ describe("McpbManifestSchema", () => { const manifestContent = readFileSync(manifestPath, "utf-8"); const manifestData = JSON.parse(manifestContent); - const result = v0_3.McpbManifestSchema.safeParse(manifestData); + const result = McpbManifestSchema.safeParse(manifestData); expect(result.success).toBe(false); if (!result.success) { @@ -134,7 +134,7 @@ describe("McpbManifestSchema", () => { }, }; - const result = v0_3.McpbManifestSchema.safeParse(fullManifest); + const result = McpbManifestSchema.safeParse(fullManifest); expect(result.success).toBe(true); if (result.success) { @@ -167,7 +167,7 @@ describe("McpbManifestSchema", () => { }, }; - const result = v0_3.McpbManifestSchema.safeParse(manifest); + const result = McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(true); }); }); @@ -270,7 +270,7 @@ describe("McpbManifestSchema", () => { default_locale: "en-US", }, }; - const result = v0_3.McpbManifestSchema.safeParse(manifest); + const result = McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(false); if (!result.success) { const messages = result.error.issues.map((issue) => issue.message); @@ -286,7 +286,7 @@ describe("McpbManifestSchema", () => { default_locale: "en_us", }, }; - const result = v0_3.McpbManifestSchema.safeParse(manifest); + const result = McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(false); }); @@ -298,7 +298,7 @@ describe("McpbManifestSchema", () => { default_locale: "en-US", }, }; - const result = v0_3.McpbManifestSchema.safeParse(manifest); + const result = McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(true); }); }); @@ -322,7 +322,7 @@ describe("McpbManifestSchema", () => { ...base, icons: [{ src: "assets/icon.png", size: "16", theme: "light" }], }; - const result = v0_3.McpbManifestSchema.safeParse(manifest); + const result = McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(false); }); @@ -331,7 +331,7 @@ describe("McpbManifestSchema", () => { ...base, icons: [{ src: "assets/icon.png", size: "128x128" }], }; - const result = v0_3.McpbManifestSchema.safeParse(manifest); + const result = McpbManifestSchema.safeParse(manifest); expect(result.success).toBe(true); }); }); diff --git a/test/server/index.js b/test/server/index.js deleted file mode 100644 index c8a782c..0000000 --- a/test/server/index.js +++ /dev/null @@ -1 +0,0 @@ -// test fixture diff --git a/test/sign.e2e.test.ts b/test/sign.e2e.test.ts index 9af52a0..cbf1c4f 100755 --- a/test/sign.e2e.test.ts +++ b/test/sign.e2e.test.ts @@ -468,50 +468,4 @@ describe("MCPB Signing E2E Tests", () => { it("should remove signatures", async () => { await testSignatureRemoval(); }); - - it("should update EOCD comment_length after signing", async () => { - const testFile = path.join(TEST_DIR, "test-eocd.mcpb"); - fs.copyFileSync(TEST_MCPB, testFile); - - // Read original EOCD comment_length - const originalContent = fs.readFileSync(testFile); - let eocdOffset = -1; - for (let i = originalContent.length - 22; i >= 0; i--) { - if (originalContent.readUInt32LE(i) === 0x06054b50) { - eocdOffset = i; - break; - } - } - expect(eocdOffset).toBeGreaterThanOrEqual(0); - const originalCommentLength = originalContent.readUInt16LE(eocdOffset + 20); - expect(originalCommentLength).toBe(0); // Fresh ZIP has no comment - - // Sign the file - signMcpbFile(testFile, SELF_SIGNED_CERT, SELF_SIGNED_KEY); - - // Read signed file and verify EOCD comment_length was updated - const signedContent = fs.readFileSync(testFile); - let signedEocdOffset = -1; - for (let i = signedContent.length - 22; i >= 0; i--) { - if (signedContent.readUInt32LE(i) === 0x06054b50) { - signedEocdOffset = i; - break; - } - } - expect(signedEocdOffset).toBeGreaterThanOrEqual(0); - const signedCommentLength = signedContent.readUInt16LE( - signedEocdOffset + 20, - ); - - // Comment length should equal everything after the EOCD record's original end - const eocdMinSize = 22; // minimum EOCD size (no comment) - const dataAfterEocd = - signedContent.length - - (signedEocdOffset + eocdMinSize + originalCommentLength); - expect(signedCommentLength).toBe(dataAfterEocd); - expect(signedCommentLength).toBeGreaterThan(0); - - // Clean up - fs.unlinkSync(testFile); - }); }); diff --git a/test/validate.test.ts b/test/validate.test.ts deleted file mode 100644 index e998fb2..0000000 --- a/test/validate.test.ts +++ /dev/null @@ -1,369 +0,0 @@ -import { execSync } from "node:child_process"; -import fs from "node:fs"; -import { join } from "node:path"; - -import { DEFAULT_MANIFEST_VERSION } from "../src/shared/constants"; - -interface ExecSyncError extends Error { - stdout: Buffer; - stderr: Buffer; - status: number | null; -} - -describe("Enhanced Validation", () => { - const cliPath = join(__dirname, "../dist/cli/cli.js"); - const fixturesDir = join(__dirname, "fixtures", "validate"); - - function createManifest( - dir: string, - overrides: Record = {}, - ) { - const manifest = { - manifest_version: DEFAULT_MANIFEST_VERSION, - name: "test-extension", - version: "1.0.0", - description: "Test extension for validation", - author: { name: "Test Author" }, - server: { - type: "node", - entry_point: "server/index.js", - mcp_config: { - command: "node", - args: ["${__dirname}/server/index.js"], - }, - }, - ...overrides, - }; - - fs.writeFileSync( - join(dir, "manifest.json"), - JSON.stringify(manifest, null, 2), - ); - } - - beforeAll(() => { - execSync("yarn build", { cwd: join(__dirname, "..") }); - - if (fs.existsSync(fixturesDir)) { - fs.rmSync(fixturesDir, { recursive: true, force: true }); - } - fs.mkdirSync(fixturesDir, { recursive: true }); - }); - - afterAll(() => { - if (fs.existsSync(fixturesDir)) { - fs.rmSync(fixturesDir, { recursive: true, force: true }); - } - }); - - describe("entry point validation", () => { - it("should fail when entry_point file is missing", () => { - const dir = join(fixturesDir, "missing-entry"); - fs.mkdirSync(dir, { recursive: true }); - createManifest(dir); - // Deliberately NOT creating server/index.js - - expect(() => { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(); - - try { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - } catch (error: unknown) { - const execError = error as ExecSyncError; - const output = execError.stdout?.toString() || ""; - expect(output).toContain("Entry point file not found"); - } - }); - - it("should warn about extension mismatch (node type with .py file)", () => { - const dir = join(fixturesDir, "ext-mismatch"); - fs.mkdirSync(join(dir, "server"), { recursive: true }); - fs.writeFileSync(join(dir, "server", "main.py"), "print('hello')"); - createManifest(dir, { - server: { - type: "node", - entry_point: "server/main.py", - mcp_config: { command: "node" }, - }, - }); - - const result = execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - }); - - expect(result).toContain("Manifest schema validation passes!"); - expect(result).toContain("Unusual entry point extension"); - }); - - it("should fail when binary entry_point is not executable (Unix only)", () => { - if (process.platform === "win32") { - return; - } - - const dir = join(fixturesDir, "binary-not-exec"); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(join(dir, "myserver"), "#!/bin/sh\necho hi"); - fs.chmodSync(join(dir, "myserver"), 0o644); - createManifest(dir, { - server: { - type: "binary", - entry_point: "myserver", - mcp_config: { command: "./myserver" }, - }, - }); - - expect(() => { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(); - - try { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - } catch (error: unknown) { - const execError = error as ExecSyncError; - const output = execError.stdout?.toString() || ""; - expect(output).toContain("not executable"); - } - }); - - it("should pass when binary entry_point is executable (Unix only)", () => { - if (process.platform === "win32") { - return; - } - - const dir = join(fixturesDir, "binary-exec"); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(join(dir, "myserver"), "#!/bin/sh\necho hi"); - fs.chmodSync(join(dir, "myserver"), 0o755); - createManifest(dir, { - server: { - type: "binary", - entry_point: "myserver", - mcp_config: { command: "./myserver" }, - }, - }); - - const result = execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - }); - - expect(result).toContain("Manifest schema validation passes!"); - }); - }); - - describe("command variable validation", () => { - it("should fail on invalid variables like ${BUNDLE_ROOT}", () => { - const dir = join(fixturesDir, "invalid-var"); - fs.mkdirSync(join(dir, "server"), { recursive: true }); - fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); - createManifest(dir, { - server: { - type: "node", - entry_point: "server/index.js", - mcp_config: { - command: "node", - args: ["${BUNDLE_ROOT}/server/index.js"], - }, - }, - }); - - expect(() => { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(); - - try { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - } catch (error: unknown) { - const execError = error as ExecSyncError; - const output = execError.stdout?.toString() || ""; - expect(output).toContain("Invalid variable"); - expect(output).toContain("${BUNDLE_ROOT}"); - } - }); - - it("should fail on invalid variables in env values", () => { - const dir = join(fixturesDir, "invalid-env-var"); - fs.mkdirSync(join(dir, "server"), { recursive: true }); - fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); - createManifest(dir, { - server: { - type: "node", - entry_point: "server/index.js", - mcp_config: { - command: "node", - args: ["${__dirname}/server/index.js"], - env: { PATH: "${HOME}/bin" }, - }, - }, - }); - - expect(() => { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(); - - try { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - } catch (error: unknown) { - const execError = error as ExecSyncError; - const output = execError.stdout?.toString() || ""; - expect(output).toContain("Invalid variable"); - expect(output).toContain("${HOME}"); - } - }); - - it("should pass with valid variables: ${__dirname} and ${user_config.*}", () => { - const dir = join(fixturesDir, "valid-vars"); - fs.mkdirSync(join(dir, "server"), { recursive: true }); - fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); - createManifest(dir, { - server: { - type: "node", - entry_point: "server/index.js", - mcp_config: { - command: "node", - args: [ - "${__dirname}/server/index.js", - "--key", - "${user_config.api_key}", - ], - env: { SEP: "${pathSeparator}" }, - }, - }, - }); - - const result = execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - }); - - expect(result).toContain("Manifest schema validation passes!"); - expect(result).not.toContain("Invalid variable"); - }); - - it("should fail on invalid variables in platform_overrides", () => { - const dir = join(fixturesDir, "invalid-platform-var"); - fs.mkdirSync(join(dir, "server"), { recursive: true }); - fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); - - const manifest = { - manifest_version: "0.3", - name: "test-extension", - version: "1.0.0", - description: "Test", - author: { name: "Test" }, - server: { - type: "node", - entry_point: "server/index.js", - mcp_config: { - command: "node", - args: ["${__dirname}/server/index.js"], - platform_overrides: { - win32: { - args: ["${APPDATA}/server/index.js"], - }, - }, - }, - }, - }; - - fs.writeFileSync( - join(dir, "manifest.json"), - JSON.stringify(manifest, null, 2), - ); - - expect(() => { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - }).toThrow(); - - try { - execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - stdio: "pipe", - }); - } catch (error: unknown) { - const execError = error as ExecSyncError; - const output = execError.stdout?.toString() || ""; - expect(output).toContain("Invalid variable"); - expect(output).toContain("${APPDATA}"); - } - }); - }); - - describe("sensitive file detection", () => { - it("should warn about credentials.json in project directory", () => { - const dir = join(fixturesDir, "sensitive-files"); - fs.mkdirSync(join(dir, "server"), { recursive: true }); - fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); - fs.writeFileSync(join(dir, "credentials.json"), '{"secret": "value"}'); - createManifest(dir); - - const result = execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - }); - - // Should pass (warnings only) but show the warning - expect(result).toContain("Manifest schema validation passes!"); - expect(result).toContain("Potentially sensitive file"); - expect(result).toContain("credentials.json"); - }); - - it("should warn about .pem files in project directory", () => { - const dir = join(fixturesDir, "sensitive-pem"); - fs.mkdirSync(join(dir, "server"), { recursive: true }); - fs.writeFileSync(join(dir, "server", "index.js"), "// fixture"); - fs.writeFileSync(join(dir, "server.pem"), "--- CERT ---"); - createManifest(dir); - - const result = execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - }); - - expect(result).toContain("Manifest schema validation passes!"); - expect(result).toContain("Potentially sensitive file"); - expect(result).toContain("server.pem"); - }); - }); - - describe("happy path", () => { - it("should pass with all files present and correct types", () => { - const dir = join(fixturesDir, "happy-path"); - fs.mkdirSync(join(dir, "server"), { recursive: true }); - fs.writeFileSync(join(dir, "server", "index.js"), "// server code"); - createManifest(dir); - - const result = execSync(`node ${cliPath} validate ${dir}`, { - encoding: "utf-8", - }); - - expect(result).toContain("Manifest schema validation passes!"); - expect(result).not.toContain("ERROR"); - expect(result).not.toContain("sensitive"); - }); - }); -}); diff --git a/yarn.lock b/yarn.lock index 538dc22..593a7a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,7 +36,7 @@ __metadata: galactus: "npm:^1.0.0" ignore: "npm:^7.0.5" jest: "npm:^29.7.0" - node-forge: "npm:^1.3.2" + node-forge: "npm:^1.3.1" prettier: "npm:^3.3.3" pretty-bytes: "npm:^5.6.0" ts-jest: "npm:^29.3.2" @@ -4739,10 +4739,10 @@ __metadata: languageName: node linkType: hard -"node-forge@npm:^1.3.2": - version: 1.3.3 - resolution: "node-forge@npm:1.3.3" - checksum: 10c0/9c6f53b0ebb34865872cf62a35b0aef8fb337e2efc766626c2e3a0040f4c02933bf29a62ba999eb44a2aca73bd512c4eda22705a47b94654b9fb8ed53db9a1db +"node-forge@npm:^1.3.1": + version: 1.3.1 + resolution: "node-forge@npm:1.3.1" + checksum: 10c0/e882819b251a4321f9fc1d67c85d1501d3004b4ee889af822fd07f64de3d1a8e272ff00b689570af0465d65d6bf5074df9c76e900e0aff23e60b847f2a46fbe8 languageName: node linkType: hard