diff --git a/CHANGELOG.md b/CHANGELOG.md index ec93124..194627f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,25 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- **Agent Plugins 1.0 MCP-only pilot.** Repository-level `plugin.json` and + `mcp.json` launch the exact published `@microsoft/spe-mcp@0.2.0-alpha.1` + package over local `stdio`, default to `--read-only`, and keep plugin-owned + token/state files in the client-managed `${PLUGIN_DATA}` directory. Includes + schema, packaging, startup, and security-contract tests plus installation and + removal documentation. No skills, remote transport, OAuth, hooks, agents, or + server behavior changes are included. The subprocess also uses + `${PLUGIN_DATA}` as its working directory so `npx` cannot confuse the plugin + source root with an installed package on Windows. +- **Release-safe plugin stamping and schema validation.** The npm version and + prepack lifecycle now keep `package.json`, `plugin.json`, and the exact MCP + package pin synchronized for prerelease and official packages. Manifest tests + validate against vendored, authoritative Agent Plugins 1.0 schemas. Safe + transitive lockfile updates clear all high-severity npm audit findings. + ## [0.2.0-alpha.1] ### Added diff --git a/PRIVACY.md b/PRIVACY.md index d3624c6..26b089f 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -29,6 +29,10 @@ to send data to Microsoft.** Specifically: **no personal, tenant, or usage information** and exists only so the service can measure aggregate traffic driven by this tool. It is a request header on calls you already make — not a separate data feed. +- **Agent Plugin package retrieval.** The optional Agent Plugins 1.0 pilot launches an exact + pinned version through `npx -y`. If the package is not cached, npm sends the package name, + version, and standard request metadata to your configured npm registry. It does not send + tenant data, credentials used by this server, MCP prompts, or tool results to the registry. See [docs/DATA-FLOW.md](docs/DATA-FLOW.md) for the full list of network endpoints and what travels to each. diff --git a/README.md b/README.md index eb3bc87..abd703a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) server for Sh - **Get started on Microsoft Learn:** [SharePoint Embedded MCP server](https://learn.microsoft.com/sharepoint/dev/embedded/getting-started/spe-mcp-server) - **SharePoint Embedded product docs:** +- **Agent Plugins 1.0 pilot:** [install, security boundaries, and removal](docs/AGENT-PLUGIN.md) - **In this repo:** [Available Tools](#available-tools) · [Configuration](#configuration) · [Security controls](docs/SECURITY-CONTROLS.md) · [Troubleshooting](docs/TROUBLESHOOTING.md) ## Available Tools @@ -71,6 +72,18 @@ The server exposes **40 tools**, plus an MCP **Prompt** (`provision_spe_app`) an Run the published npm package directly from your MCP client with `npx`; no global install is required. +### Agent Plugins 1.0 (read-only pilot) + +The repository root is also an Agent Plugins 1.0 package. A compatible client +discovers `plugin.json` and the local `stdio` definition in `mcp.json`. The +plugin pins `@microsoft/spe-mcp@0.2.0-alpha.1`, starts it with `--read-only`, +and stores durable token/state files under the client-managed `${PLUGIN_DATA}` +directory. It includes no skill, remote transport, OAuth, hook, or custom +agent. + +See [Agent Plugins 1.0 MCP-only pilot](docs/AGENT-PLUGIN.md) for installation, +security boundaries, limitations, validation, and removal. + ### VS Code / Cursor Add an MCP server entry to `.vscode/mcp.json` (VS Code) or your Cursor MCP @@ -113,7 +126,7 @@ Add to `%APPDATA%\Claude\claude_desktop_config.json` (Windows) or Because clients run the package through `npx`, they pick up published updates without a global install. Pin a specific version with -`@microsoft/spe-mcp@0.1.0-alpha.1`. To remove the server, delete the MCP +`@microsoft/spe-mcp@0.2.0-alpha.1`. To remove the server, delete the MCP client config entry. ## Prerequisites @@ -540,12 +553,6 @@ Microsoft takes security seriously. If you believe you have found a security vulnerability, please report it privately as described in [SECURITY.md](SECURITY.md) — **do not** file a public GitHub issue. - ## Important notices > **Preview software.** `@microsoft/spe-mcp` is an early (alpha) preview released for diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index 9c9b0ca..ed29f58 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -75,7 +75,7 @@ SOFTWARE. --------------------------------------------------------------- -3. @hono/node-server 1.19.14 (MIT) +3. @hono/node-server 2.0.12 (MIT) https://github.com/honojs/node-server MIT License @@ -102,7 +102,7 @@ SOFTWARE. --------------------------------------------------------------- -4. @modelcontextprotocol/sdk 1.29.0 (MIT) +4. @modelcontextprotocol/sdk 1.30.0 (MIT) https://github.com/modelcontextprotocol/typescript-sdk MIT License @@ -1245,7 +1245,7 @@ SOFTWARE. --------------------------------------------------------------- -41. fast-uri 3.1.2 (BSD-3-Clause) +41. fast-uri 3.1.5 (BSD-3-Clause) https://github.com/fastify/fast-uri Copyright (c) 2011-2021, Gary Court until https://github.com/garycourt/uri-js/commit/a1acf730b4bba3f1097c9f52e7d9d3aba8cdcaae @@ -1526,7 +1526,7 @@ SOFTWARE. --------------------------------------------------------------- -51. hono 4.12.26 (MIT) +51. hono 4.12.34 (MIT) https://github.com/honojs/hono MIT License @@ -1628,8 +1628,8 @@ PERFORMANCE OF THIS SOFTWARE. --------------------------------------------------------------- -55. ip-address 10.2.0 (MIT) -git://github.com/beaugunderson/ip-address +55. ip-address 10.4.0 (MIT) +https://github.com/beaugunderson/ip-address Copyright (C) 2011 by Beau Gunderson @@ -3401,3 +3401,210 @@ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. --------------------------------------------------------------- + +121. Agent Plugins 1.0 JSON schemas (Apache-2.0) +https://github.com/agentplugins/agent-plugins-spec + +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 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +--------------------------------------------------------------- diff --git a/docs/AGENT-PLUGIN.md b/docs/AGENT-PLUGIN.md new file mode 100644 index 0000000..844c087 --- /dev/null +++ b/docs/AGENT-PLUGIN.md @@ -0,0 +1,151 @@ +# Agent Plugins 1.0 MCP-only pilot + +This repository is an [Agent Plugins 1.0](https://agent-plugins.org/) package +that provides one local SharePoint Embedded MCP server. The pilot is additive: +it does not change the server, add skills, agents, hooks, OAuth, or a remote +transport. + +## Install the pilot before merge + +Prerequisites: + +- Node.js 22, 24, or 26 +- `npx` on `PATH` +- Azure CLI authentication as described in the main [README](../README.md) +- an Agent Plugins 1.0 client, such as a current VS Code release with + `chat.plugins.enabled` + +The pilot manifests are currently on PR +[#82](https://github.com/microsoft/SharePoint-Embedded-MCP-Server/pull/82), +not on the repository's default `main` branch. Clone the canonical repository +and check out the PR head explicitly: + +```bash +git clone https://github.com/microsoft/SharePoint-Embedded-MCP-Server.git +cd SharePoint-Embedded-MCP-Server +git fetch origin pull/82/head:pilot/agent-plugin +git switch pilot/agent-plugin +git show --no-patch --oneline HEAD +test -f plugin.json && test -f mcp.json +``` + +On PowerShell, use `Test-Path .\plugin.json` and +`Test-Path .\mcp.json` for the final check. + +Register the absolute path of that checked-out repository with the supported +VS Code `chat.pluginLocations` setting: + +```json +{ + "chat.pluginLocations": { + "/absolute/path/to/SharePoint-Embedded-MCP-Server": true + } +} +``` + +Reload VS Code, then run **MCP: List Servers** and confirm +`sharepoint-embedded` appears. The registered directory must remain checked out +at the PR head while testing the pilot. + +When enabled, the client reads `mcp.json` and starts: + +```text +npx -y @microsoft/spe-mcp@0.2.0-alpha.1 start --read-only --data-dir ${PLUGIN_DATA} +``` + +The client expands `cwd` to `${PLUGIN_DATA}` before launch. This avoids npm +self-package resolution when the plugin root is itself the +`@microsoft/spe-mcp` source tree, while preserving the exact command and local +`stdio` transport. + +The exact npm version was verified as published on August 7, 2026. It is +intentionally pinned: plugin updates, not npm dist-tag movement, control server +updates. + +## Install from the repository after merge to main + +**Future instruction — do not use until the plugin manifests reach the default +`main` branch.** After that merge, VS Code's **Chat: Install Plugin From +Source** command can use: + +```text +https://github.com/microsoft/SharePoint-Embedded-MCP-Server +``` + +Before the merge, that plain URL resolves `main`, where the plugin manifests +are absent, so use the explicit PR checkout and `chat.pluginLocations` flow +above. + +## Security boundaries + +- **Read-only by default:** `--read-only` hides and rejects every tool not + annotated read-only. Installing this pilot does not enable tenant writes. +- **Local process only:** the plugin uses MCP `stdio`; it defines no remote URL. +- **User credentials:** read operations still act with the signed-in user's + permissions. Review the tenant and account before using a tool. +- **Executable trust:** enabling the plugin allows the client to run the pinned + public npm package through `npx`. Review the repository and package publisher + before installation. +- **No autonomous extensions:** the package contains no skills, custom agents, + hooks, or OAuth configuration. + +See [Security controls](SECURITY-CONTROLS.md) for the server's complete controls +and limitations. + +## Persistent plugin data + +Agent Plugins 1.0 clients create a private, writable `${PLUGIN_DATA}` directory +for each installed plugin and preserve it across plugin updates. The standard +expands `${PLUGIN_DATA}` in `args`; it does not expand placeholders in +`command`. Accordingly, `mcp.json` keeps `command` as the single executable +token `npx` and passes `${PLUGIN_DATA}` as the separate value for `--data-dir`. +It also uses `${PLUGIN_DATA}` as `cwd`; the standard requires clients to create +that directory before launching the subprocess and supports placeholder +expansion in `cwd` on Windows, macOS, and Linux. + +The SPE MCP server stores its token cache and provisioning state under that +directory. Disabling the plugin stops the MCP process but preserves this state. +Uninstalling/removing the plugin may remove client-managed plugin data; sign in +again if the client removes it. + +## Limitations + +- This pilot does not expose provisioning, upload, permission, delete, or other + mutating tools. +- There is no remote MCP transport or plugin-provided OAuth flow. +- `npx` may need network access on first launch to retrieve the exact package. +- Agent Plugins 1.0 client support and organization policy determine whether + the plugin can be enabled. + +## Validate + +```bash +npm install +npm run test -- src/agent-plugin.test.ts +npm run ci +``` + +The contract tests validate the closed 1.0.0 manifest fields, exact npm pin, +package/lockfile alignment, local stdio startup, read-only tool exposure, and +`${PLUGIN_DATA}` configuration. + +The authoritative Agent Plugins 1.0 JSON schemas are vendored under +`schemas/agent-plugins/1.0.0` for deterministic offline validation. They are +unmodified Apache-2.0 software material and are excluded from the npm package. +The npm `version` lifecycle synchronizes `plugin.json` and the exact package pin +in `mcp.json`; `prepack` fails if a release stamp leaves them out of sync. + +## Disable, remove, or roll back + +Disable or uninstall **sharepoint-embedded-mcp** from the client's Agent +Plugins view. In VS Code, remove a local `chat.pluginLocations` entry if one was +used. For a repository rollback, revert `plugin.json`, `mcp.json`, and the +associated package/docs/test changes; the standalone SPE MCP server remains +unchanged. + +## Standards references + +- [Agent Plugins 1.0 specification](https://github.com/agentplugins/agent-plugins-spec/blob/main/spec/1.0.0.md) +- [`plugin.json` schema](https://agent-plugins.org/schemas/1.0.0/plugin.schema.json) +- [`mcp.json` schema](https://agent-plugins.org/schemas/1.0.0/mcp.schema.json) +- [Agent plugins in VS Code](https://code.visualstudio.com/docs/agent-customization/agent-plugins) diff --git a/docs/DATA-FLOW.md b/docs/DATA-FLOW.md index 8dbb395..c523953 100644 --- a/docs/DATA-FLOW.md +++ b/docs/DATA-FLOW.md @@ -8,11 +8,16 @@ travels there, and how that maps to Microsoft compliance boundaries. It backs th ## Topology ``` -MCP client <--stdio--> spe-mcp-server (local process) <--HTTPS--> Microsoft endpoints +Agent Plugin client --HTTPS--> npm registry + | + +--stdio--> spe-mcp-server (local process) --HTTPS--> Microsoft endpoints ``` - The server is a **local** process. It talks to your MCP client over **stdio**; it opens no network socket for the client connection. +- The Agent Plugin pilot launches the pinned package with `npx -y`. On first use, npm may + retrieve that package and its dependencies from the configured npm registry before the + local process starts. No tenant data or MCP prompts are sent to the registry. - Every outbound network call is HTTPS to a **Microsoft-operated** endpoint, made **on your behalf**, using **your** credentials, into **your** tenant and subscription. @@ -24,10 +29,13 @@ MCP client <--stdio--> spe-mcp-server (local process) <--HTTPS--> Microsoft | Microsoft Graph (`graph.microsoft.com`) | Create/manage app registrations, container types, containers, and content | Your delegated token | The requests you invoke, in your tenant | Microsoft first-party, in-tenant | | Azure Resource Manager (`management.azure.com`) | Register the `Microsoft.Syntex` provider and wire SPE billing to your subscription | Your Azure token | ARM requests in your subscription | Microsoft first-party, in-subscription | | Microsoft Learn MCP (`learn.microsoft.com/api/mcp`) | Read-only public documentation lookup (`docs_search`) | **None** | Documentation queries only — **no customer data** | Microsoft first-party, public docs | +| Configured npm registry (Agent Plugin pilot only) | Retrieve the exact pinned `@microsoft/spe-mcp` package and dependencies for local execution | npm client configuration | Package name/version and standard npm request metadata; no tenant data or MCP prompts | External software-supply-chain boundary | -The server contacts **no non-Microsoft services**. The Microsoft Learn documentation lookup -is the only unauthenticated, out-of-tenant call; it carries no customer data, is host- -validated before use (control **SEC-007**), and can be disabled with `--tools`. +The running server contacts no non-Microsoft services. Before startup, the Agent Plugin's +`npx` launcher may contact the user's configured npm registry to retrieve the pinned package. +The Microsoft Learn documentation lookup is the only unauthenticated, out-of-tenant server +call; it carries no customer data, is host-validated before use (control **SEC-007**), and +can be disabled with `--tools`. ## Local artifacts diff --git a/docs/SECURITY-CONTROLS.md b/docs/SECURITY-CONTROLS.md index 56f5d63..caf4006 100644 --- a/docs/SECURITY-CONTROLS.md +++ b/docs/SECURITY-CONTROLS.md @@ -25,5 +25,12 @@ that maps each code to a human-readable name and a one-line description. | SEC-003 | Secure filesystem (owner-only) | Credential and state files (token cache, server state) are written owner-only (POSIX `0o600`; ACL-governed on Windows). | | SEC-007 | Docs endpoint validation | The Microsoft Learn MCP endpoint is resolved and validated before use to prevent redirection to an untrusted host. | +## Agent Plugin trust boundary + +The optional Agent Plugins 1.0 pilot launches an immutable package version with `npx -y`. +This creates a software-supply-chain boundary with the user's configured npm registry before +the local stdio server starts. Users should verify the package publisher and pinned version; +the pilot then confines state to `${PLUGIN_DATA}` and starts the server in read-only mode. + > Adding a new safeguard? Give it the next code in its family and add a row here > so code comments and tests have a lookup. diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000..4bfc20b --- /dev/null +++ b/mcp.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "mcpServers": { + "sharepoint-embedded": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@microsoft/spe-mcp@0.2.0-alpha.1", + "start", + "--read-only", + "--data-dir", + "${PLUGIN_DATA}" + ], + "cwd": "${PLUGIN_DATA}" + } + } +} diff --git a/package-lock.json b/package-lock.json index c0c78e0..3ea334e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@microsoft/spe-mcp", - "version": "0.1.0-alpha.1", + "version": "0.2.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@microsoft/spe-mcp", - "version": "0.1.0-alpha.1", + "version": "0.2.0-alpha.1", "license": "MIT", "dependencies": { "@azure/msal-node": "^2.6.0", @@ -26,6 +26,7 @@ "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", "@vitest/coverage-v8": "^3.2.4", + "ajv": "^8.20.0", "eslint": "^9.39.4", "typescript": "^5.3.0", "vitest": "^3.2.4" @@ -625,9 +626,9 @@ "license": "MIT" }, "node_modules/@eslint/config-array/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -723,9 +724,9 @@ "license": "MIT" }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -801,12 +802,12 @@ } }, "node_modules/@hono/node-server": { - "version": "1.19.14", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", - "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.0.12.tgz", + "integrity": "sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==", "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" @@ -959,12 +960,12 @@ "dev": true }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", - "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -1997,16 +1998,16 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/buffer-equal-constant-time": { @@ -2587,9 +2588,9 @@ "license": "MIT" }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "dev": true, "license": "MIT", "dependencies": { @@ -2849,9 +2850,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "funding": [ { "type": "github", @@ -3093,9 +3094,9 @@ "license": "MIT" }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -3178,9 +3179,9 @@ } }, "node_modules/hono": { - "version": "4.12.26", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", - "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", + "version": "4.12.34", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.34.tgz", + "integrity": "sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==", "license": "MIT", "engines": { "node": ">=16.9.0" @@ -3273,9 +3274,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", - "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.4.0.tgz", + "integrity": "sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==", "license": "MIT", "engines": { "node": ">= 12" @@ -3470,9 +3471,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", "dev": true, "funding": [ { @@ -3790,9 +3791,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "dev": true, "funding": [ { @@ -4056,9 +4057,9 @@ } }, "node_modules/postcss": { - "version": "8.5.15", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", - "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "version": "8.5.25", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz", + "integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==", "dev": true, "funding": [ { @@ -4076,7 +4077,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/package.json b/package.json index 8286f7c..5cdd6d0 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,10 @@ "files": [ "dist", "samples", + "plugin.json", + "mcp.json", + "docs/AGENT-PLUGIN.md", + "scripts/sync-plugin-version.mjs", "README.md", "LICENSE", "THIRD-PARTY-NOTICES" @@ -49,6 +53,8 @@ "lint": "eslint src", "typecheck": "tsc --noEmit", "notices": "node scripts/generate-third-party-notices.mjs", + "version": "node scripts/sync-plugin-version.mjs && git add plugin.json mcp.json server.json package-lock.json", + "prepack": "node scripts/sync-plugin-version.mjs --check", "prepublishOnly": "npm run build", "ci": "npm run typecheck && npm run build && npm run test" }, @@ -67,6 +73,7 @@ "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.57.2", "@vitest/coverage-v8": "^3.2.4", + "ajv": "^8.20.0", "eslint": "^9.39.4", "typescript": "^5.3.0", "vitest": "^3.2.4" diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..4015877 --- /dev/null +++ b/plugin.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "sharepoint-embedded-mcp", + "version": "0.2.0-alpha.1", + "description": "Read-only SharePoint Embedded MCP server for Agent Plugins 1.0 clients.", + "author": { + "name": "Microsoft Corporation", + "url": "https://www.microsoft.com" + }, + "homepage": "https://learn.microsoft.com/sharepoint/dev/embedded/getting-started/spe-mcp-server", + "repository": "https://github.com/microsoft/SharePoint-Embedded-MCP-Server", + "license": "MIT", + "keywords": [ + "mcp", + "sharepoint-embedded", + "agent-plugins" + ] +} diff --git a/schemas/agent-plugins/1.0.0/mcp.schema.json b/schemas/agent-plugins/1.0.0/mcp.schema.json new file mode 100644 index 0000000..a9139a4 --- /dev/null +++ b/schemas/agent-plugins/1.0.0/mcp.schema.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "title": "Agent Plugins MCP Configuration", + "description": "Machine-readable schema for mcp.json in Agent Plugins 1.0.0. The Agent Plugins specification defines additional semantic and operational requirements.", + "type": "object", + "properties": { + "$schema": { + "const": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json", + "description": "Canonical identifier of the MCP configuration schema for the Agent Plugins version targeted by this document." + }, + "mcpServers": { + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/server" + } + } + }, + "required": ["$schema", "mcpServers"], + "additionalProperties": false, + "$defs": { + "server": { + "title": "MCP server", + "oneOf": [ + { + "$ref": "#/$defs/stdioServer" + }, + { + "$ref": "#/$defs/streamableHttpServer" + }, + { + "$ref": "#/$defs/sseServer" + } + ] + }, + "stdioServer": { + "title": "stdio MCP server", + "type": "object", + "properties": { + "type": { + "const": "stdio" + }, + "command": { + "type": "string", + "minLength": 1, + "description": "Executable token. Resolution rules are defined by the Agent Plugins specification." + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + }, + "env": { + "type": "object", + "propertyNames": { + "not": { + "enum": ["PLUGIN_ROOT", "PLUGIN_DATA"] + } + }, + "additionalProperties": { + "type": "string" + } + }, + "cwd": { + "type": "string", + "pattern": "^(?:\\./|\\$\\{PLUGIN_ROOT\\}(?:/|$)|\\$\\{PLUGIN_DATA\\}(?:/|$))", + "description": "Plugin-relative, PLUGIN_ROOT-rooted, or PLUGIN_DATA-rooted working directory. Filesystem containment is validated separately." + } + }, + "required": ["type", "command"], + "additionalProperties": false + }, + "streamableHttpServer": { + "title": "Streamable HTTP MCP server", + "type": "object", + "properties": { + "type": { + "const": "streamable-http" + }, + "url": { + "type": "string", + "minLength": 1, + "description": "MCP endpoint URL. URL semantics are defined by the Agent Plugins specification." + }, + "headers": { + "$ref": "#/$defs/headers" + } + }, + "required": ["type", "url"], + "additionalProperties": false + }, + "sseServer": { + "title": "Legacy HTTP+SSE MCP server", + "type": "object", + "properties": { + "type": { + "const": "sse" + }, + "url": { + "type": "string", + "minLength": 1, + "description": "MCP endpoint URL. URL semantics are defined by the Agent Plugins specification." + }, + "headers": { + "$ref": "#/$defs/headers" + } + }, + "required": ["type", "url"], + "additionalProperties": false + }, + "headers": { + "title": "HTTP headers", + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } +} diff --git a/schemas/agent-plugins/1.0.0/plugin.schema.json b/schemas/agent-plugins/1.0.0/plugin.schema.json new file mode 100644 index 0000000..8fed0e1 --- /dev/null +++ b/schemas/agent-plugins/1.0.0/plugin.schema.json @@ -0,0 +1,65 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "title": "Agent Plugins Manifest", + "description": "Machine-readable schema for plugin.json in Agent Plugins 1.0.0. The Agent Plugins specification defines additional semantic and operational requirements.", + "type": "object", + "properties": { + "$schema": { + "const": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "description": "Canonical identifier of the plugin manifest schema for the Agent Plugins version targeted by this document." + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^(?!.*(?:--|\\.\\.))[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$", + "description": "Human-readable plugin name." + }, + "version": { + "type": "string" + }, + "description": { + "type": "string" + }, + "author": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "url": { + "type": "string" + } + }, + "additionalProperties": false + }, + "homepage": { + "type": "string" + }, + "repository": { + "type": "string" + }, + "license": { + "type": "string" + }, + "keywords": { + "type": "array", + "items": { + "type": "string" + } + }, + "extensions": { + "type": "object", + "description": "Client-specific manifest data keyed by reverse-domain extension namespace. Agent Plugins assigns no semantics to namespace object contents.", + "additionalProperties": { + "type": "object" + } + } + }, + "required": ["$schema", "name"], + "additionalProperties": false +} diff --git a/schemas/agent-plugins/LICENSE-Apache-2.0.txt b/schemas/agent-plugins/LICENSE-Apache-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/schemas/agent-plugins/LICENSE-Apache-2.0.txt @@ -0,0 +1,202 @@ + + 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 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 + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/schemas/agent-plugins/README.md b/schemas/agent-plugins/README.md new file mode 100644 index 0000000..1b02a1b --- /dev/null +++ b/schemas/agent-plugins/README.md @@ -0,0 +1,20 @@ +# Vendored Agent Plugins schemas + +The JSON schemas under `1.0.0/` are unmodified copies from the +[Agent Plugins specification repository](https://github.com/agentplugins/agent-plugins-spec) +at commit [`bd383552095128f6effe895b9257cfd580a6d179`](https://github.com/agentplugins/agent-plugins-spec/commit/bd383552095128f6effe895b9257cfd580a6d179): + +- `schemas/1.0.0/plugin.schema.json` +- `schemas/1.0.0/mcp.schema.json` + +They are vendored so CI validates manifests deterministically without network +access. Agent Plugins schemas are software material licensed under Apache-2.0; +the upstream license is reproduced in `LICENSE-Apache-2.0.txt`. + +SHA-256 at the pinned upstream commit: + +- `plugin.schema.json`: `0A4AAD95CE337878AD38802EBF0DAA3FDE76ABE3F65400C86BCBB1EC0B3AB883` +- `mcp.schema.json`: `6539175BFCDF43085855183E86DA40EA94B166547A72B47AE9A0A390516D3ACB` + +These test fixtures are repository-only and are not included in the published +`@microsoft/spe-mcp` npm package. diff --git a/scripts/generate-third-party-notices.mjs b/scripts/generate-third-party-notices.mjs index 189e09c..6aa5a67 100644 --- a/scripts/generate-third-party-notices.mjs +++ b/scripts/generate-third-party-notices.mjs @@ -20,6 +20,7 @@ import { fileURLToPath } from "node:url"; const pkgRoot = dirname(dirname(fileURLToPath(import.meta.url))); const outFile = join(pkgRoot, "THIRD-PARTY-NOTICES"); +const agentPluginsLicense = join(pkgRoot, "schemas", "agent-plugins", "LICENSE-Apache-2.0.txt"); const LICENSE_FILE_RE = /^(LICENSE|LICENCE|COPYING|NOTICE)(\.|$)/i; @@ -154,6 +155,19 @@ const blocks = packages.map((p, i) => { return lines.join("\n"); }); -const body = `${header}\n${blocks.join("\n")}\n${SEP}\n`; +const vendoredBlocks = []; +if (existsSync(agentPluginsLicense)) { + vendoredBlocks.push([ + SEP, + "", + `${packages.length + 1}. Agent Plugins 1.0 JSON schemas (Apache-2.0)`, + "https://github.com/agentplugins/agent-plugins-spec", + "", + readFileSync(agentPluginsLicense, "utf8").trim(), + "", + ].join("\n")); +} + +const body = `${header}\n${blocks.concat(vendoredBlocks).join("\n")}\n${SEP}\n`; writeFileSync(outFile, body, "utf8"); console.log(`Wrote ${outFile} with ${packages.length} third-party packages.`); diff --git a/scripts/sync-plugin-version.mjs b/scripts/sync-plugin-version.mjs new file mode 100644 index 0000000..a706306 --- /dev/null +++ b/scripts/sync-plugin-version.mjs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { readFileSync, writeFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const PACKAGE_NAME = "@microsoft/spe-mcp"; + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +function serialized(value) { + return `${JSON.stringify(value, null, 2)}\n`; +} + +export function expectedPackageSpec(version) { + return `${PACKAGE_NAME}@${version}`; +} + +export function synchronizePluginVersion(root, { check = false } = {}) { + const packagePath = resolve(root, "package.json"); + const lockPath = resolve(root, "package-lock.json"); + const pluginPath = resolve(root, "plugin.json"); + const mcpPath = resolve(root, "mcp.json"); + const serverPath = resolve(root, "server.json"); + const pkg = readJson(packagePath); + const lock = readJson(lockPath); + const plugin = readJson(pluginPath); + const mcp = readJson(mcpPath); + const registry = readJson(serverPath); + const server = mcp.mcpServers?.["sharepoint-embedded"]; + + if (typeof pkg.version !== "string" || pkg.version.length === 0) { + throw new Error("package.json must contain a non-empty version"); + } + if (!server || !Array.isArray(server.args)) { + throw new Error("mcp.json must define mcpServers.sharepoint-embedded.args"); + } + + const packageIndexes = server.args + .map((arg, index) => ({ arg, index })) + .filter(({ arg }) => typeof arg === "string" && arg.startsWith(`${PACKAGE_NAME}@`)) + .map(({ index }) => index); + if (packageIndexes.length !== 1) { + throw new Error(`mcp.json must contain exactly one exact ${PACKAGE_NAME}@ argument`); + } + + const packageSpec = expectedPackageSpec(pkg.version); + const synchronized = + lock.version === pkg.version && + lock.packages?.[""]?.version === pkg.version && + plugin.version === pkg.version && + registry.version === pkg.version && + registry.packages?.every((entry) => entry.version === pkg.version) && + server.args[packageIndexes[0]] === packageSpec; + + if (check) { + if (!synchronized) { + throw new Error( + `Plugin version drift: expected package-lock.json, plugin.json, and server.json ${pkg.version}, and mcp.json ${packageSpec}`, + ); + } + return false; + } + + lock.version = pkg.version; + if (!lock.packages?.[""]) { + throw new Error('package-lock.json must define packages[""]'); + } + lock.packages[""].version = pkg.version; + plugin.version = pkg.version; + registry.version = pkg.version; + for (const entry of registry.packages ?? []) entry.version = pkg.version; + server.args[packageIndexes[0]] = packageSpec; + writeFileSync(lockPath, serialized(lock), "utf8"); + writeFileSync(pluginPath, serialized(plugin), "utf8"); + writeFileSync(mcpPath, serialized(mcp), "utf8"); + writeFileSync(serverPath, serialized(registry), "utf8"); + return !synchronized; +} + +function parseArgs(argv) { + let root = process.cwd(); + let check = false; + for (let index = 0; index < argv.length; index += 1) { + if (argv[index] === "--check") { + check = true; + } else if (argv[index] === "--root") { + root = argv[index + 1]; + if (!root) throw new Error("--root requires a path"); + index += 1; + } else { + throw new Error(`Unknown argument: ${argv[index]}`); + } + } + return { root, check }; +} + +const invokedPath = process.argv[1] ? resolve(process.argv[1]) : ""; +if (invokedPath === fileURLToPath(import.meta.url)) { + const { root, check } = parseArgs(process.argv.slice(2)); + synchronizePluginVersion(root, { check }); +} diff --git a/server.json b/server.json index d7cee05..c469bcd 100644 --- a/server.json +++ b/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "com.microsoft/sharepointembedded-mcp-server", "description": "MCP server for SharePoint Embedded management", - "version": "0.1.0-alpha.1", + "version": "0.2.0-alpha.1", "repository": { "url": "https://github.com/microsoft/SharePoint-Embedded-MCP-Server", "source": "github" @@ -13,7 +13,7 @@ "registryType": "npm", "registryBaseUrl": "https://registry.npmjs.org", "identifier": "@microsoft/spe-mcp", - "version": "0.1.0-alpha.1", + "version": "0.2.0-alpha.1", "runtimeHint": "npx", "transport": { "type": "stdio" diff --git a/src/agent-plugin.test.ts b/src/agent-plugin.test.ts new file mode 100644 index 0000000..65ba147 --- /dev/null +++ b/src/agent-plugin.test.ts @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Agent Plugins 1.0 packaging and launch-contract tests. + * + * Schema assertions use deterministic vendored 1.0.0 schemas. The protocol + * regression intentionally exercises the exact immutable npm pin from the + * manifest, with bounded fetch and startup timeouts. + */ +import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { setTimeout as delay } from "node:timers/promises"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import Ajv2020 from "ajv/dist/2020.js"; +import { Client } from "@modelcontextprotocol/sdk/client/index.js"; +import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const PLUGIN_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json"; +const MCP_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json"; + +interface PluginManifest { + [key: string]: unknown; + $schema: string; + name: string; + version: string; + author: Record; +} + +interface StdioServerConfig { + [key: string]: unknown; + type: string; + command: string; + args: string[]; + cwd: string; +} + +interface McpManifest { + [key: string]: unknown; + $schema: string; + mcpServers: Record; +} + +interface PackageManifest { + version: string; + files: string[]; +} + +interface PackageLock { + version: string; + packages: Record; +} + +function readJson(relativePath: string): T { + return JSON.parse(readFileSync(join(REPO_ROOT, relativePath), "utf8")) as T; +} + +async function removeDirectoryAfterProcessExit(path: string): Promise { + for (let attempt = 0; attempt < 20; attempt += 1) { + try { + rmSync(path, { recursive: true, force: true }); + return; + } catch (error) { + if ((error as NodeJS.ErrnoException).code !== "EPERM" || attempt === 19) { + throw error; + } + // On Windows, npm's grandchild can briefly retain its cwd after the MCP + // transport closes. Wait for that bounded shutdown before removing state. + await delay(250); + } + } +} + +const plugin = readJson("plugin.json"); +const mcp = readJson("mcp.json"); +const pkg = readJson("package.json"); +const lock = readJson("package-lock.json"); +const server = mcp.mcpServers?.["sharepoint-embedded"]; +const pinnedPackage = `@microsoft/spe-mcp@${pkg.version}`; + +describe("Agent Plugins 1.0 manifest schema", () => { + const ajv = new Ajv2020({ allErrors: true, strict: true }); + const pluginSchema = readJson>( + "schemas/agent-plugins/1.0.0/plugin.schema.json", + ); + const mcpSchema = readJson>( + "schemas/agent-plugins/1.0.0/mcp.schema.json", + ); + const validatePlugin = ajv.compile(pluginSchema); + const validateMcp = ajv.compile(mcpSchema); + + it("validates plugin.json against the vendored authoritative schema", () => { + expect(plugin.$schema).toBe(PLUGIN_SCHEMA); + expect(validatePlugin(plugin), JSON.stringify(validatePlugin.errors)).toBe(true); + }); + + it("validates mcp.json against the vendored authoritative schema", () => { + expect(mcp.$schema).toBe(MCP_SCHEMA); + expect(validateMcp(mcp), JSON.stringify(validateMcp.errors)).toBe(true); + }); + + it("uses the authoritative closed-schema behavior", () => { + const invalidPlugin = { ...plugin, unsupported: true }; + const invalidMcp = { ...mcp, unsupported: true }; + expect(validatePlugin(invalidPlugin)).toBe(false); + expect(validateMcp(invalidMcp)).toBe(false); + }); +}); + +describe("Agent Plugins 1.0 packaging contract", () => { + it("ships both manifests and the dedicated documentation", () => { + for (const file of ["plugin.json", "mcp.json", "docs/AGENT-PLUGIN.md"]) { + expect(pkg.files).toContain(file); + expect(existsSync(join(REPO_ROOT, file))).toBe(true); + } + }); + + it("pins the verified published server version without a range or tag", () => { + expect(server.args).toEqual([ + "-y", + pinnedPackage, + "start", + "--read-only", + "--data-dir", + "${PLUGIN_DATA}", + ]); + expect(server.args.filter((arg) => arg === pinnedPackage)).toHaveLength(1); + expect(lock.version).toBe(pkg.version); + expect(lock.packages[""].version).toBe(pkg.version); + }); + + it("is MCP-only with a read-only local stdio default and persistent state", () => { + expect(Object.keys(mcp.mcpServers)).toEqual(["sharepoint-embedded"]); + expect(server.type).toBe("stdio"); + expect(server.command).toBe("npx"); + expect(server).not.toHaveProperty("url"); + expect(server).not.toHaveProperty("headers"); + expect(server.args).toContain("--read-only"); + expect(server.cwd).toBe("${PLUGIN_DATA}"); + expect(server.args.slice(server.args.indexOf("--data-dir"))).toEqual([ + "--data-dir", + "${PLUGIN_DATA}", + ]); + expect(plugin).not.toHaveProperty("skills"); + expect(plugin).not.toHaveProperty("agents"); + expect(plugin).not.toHaveProperty("hooks"); + expect(plugin).not.toHaveProperty("oauth"); + }); + + it("documents a branch-aware pilot install without claiming main is ready", () => { + const docs = readFileSync(join(REPO_ROOT, "docs", "AGENT-PLUGIN.md"), "utf8"); + const futureHeading = docs.indexOf("## Install from the repository after merge to main"); + const sourceCommand = docs.indexOf("Chat: Install Plugin From"); + + expect(docs).toContain("git fetch origin pull/82/head:pilot/agent-plugin"); + expect(docs).toContain("git switch pilot/agent-plugin"); + expect(docs).toContain("chat.pluginLocations"); + expect(docs).toContain("MCP: List Servers"); + expect(futureHeading).toBeGreaterThan(0); + expect(sourceCommand).toBeGreaterThan(futureHeading); + expect(docs).toContain("do not use until the plugin manifests reach the default"); + }); +}); + +describe("Agent Plugins 1.0 exact manifest stdio launch", () => { + let client: Client; + let transport: StdioClientTransport; + let pluginData: string; + + beforeAll(async () => { + // Vitest starts in the plugin root, reproducing the package-self-resolution + // condition that fails on Windows when cwd is omitted. A conforming client + // creates PLUGIN_DATA, expands cwd/args, and launches the exact manifest + // command from that non-package directory. + expect(process.cwd()).toBe(REPO_ROOT); + pluginData = mkdtempSync(join(tmpdir(), "spe-agent-plugin-data-")); + const launchArgs = server.args.map((arg: string) => + arg.replaceAll("${PLUGIN_DATA}", pluginData), + ); + const launchCwd = server.cwd.replaceAll("${PLUGIN_DATA}", pluginData); + expect(launchCwd).toBe(pluginData); + const env = Object.fromEntries( + Object.entries(process.env).filter( + (entry): entry is [string, string] => typeof entry[1] === "string", + ), + ); + env.PLUGIN_ROOT = REPO_ROOT; + env.PLUGIN_DATA = pluginData; + env.npm_config_audit = "false"; + env.npm_config_fetch_retries = "0"; + env.npm_config_fetch_timeout = "15000"; + env.npm_config_fund = "false"; + env.npm_config_update_notifier = "false"; + + transport = new StdioClientTransport({ + command: server.command, + args: launchArgs, + cwd: launchCwd, + env, + stderr: "ignore", + }); + client = new Client({ name: "spe-agent-plugin-contract", version: "1.0.0" }, {}); + await client.connect(transport); + }, 90000); + + afterAll(async () => { + await client?.close().catch(() => undefined); + await transport?.close().catch(() => undefined); + if (pluginData) { + await removeDirectoryAfterProcessExit(pluginData); + } + }); + + it("starts the exact pinned npx command and advertises only read-only tools", async () => { + expect(client.getServerVersion()?.name).toBe("spe-mcp-server"); + const { tools } = await client.listTools(undefined, { timeout: 8000 }); + expect(tools.length).toBeGreaterThan(0); + expect(tools.every((tool) => tool.annotations?.readOnlyHint === true)).toBe(true); + expect(tools.map((tool) => tool.name)).not.toContain("container_delete"); + }); +}); diff --git a/src/packaging.test.ts b/src/packaging.test.ts index 82578b6..662550c 100644 --- a/src/packaging.test.ts +++ b/src/packaging.test.ts @@ -80,6 +80,14 @@ describe("packaging: complete metadata", () => { expect(pkg.repository.url.length).toBeGreaterThan(0); }); + describe("packaging: release version synchronization", () => { + it("runs plugin synchronization during npm version and checks it before packing", () => { + expect(pkg.scripts?.version).toContain("sync-plugin-version.mjs"); + expect(pkg.scripts?.prepack).toBe("node scripts/sync-plugin-version.mjs --check"); + expect(pkg.files).toContain("scripts/sync-plugin-version.mjs"); + }); + }); + it("has bugs, homepage and author", () => { expect(pkg.bugs?.url ?? pkg.bugs).toBeTruthy(); expect(pkg.homepage).toBeTruthy(); diff --git a/src/plugin-version-sync.test.ts b/src/plugin-version-sync.test.ts new file mode 100644 index 0000000..15808e4 --- /dev/null +++ b/src/plugin-version-sync.test.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { execFileSync, spawnSync } from "node:child_process"; +import { cpSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; + +const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const SCRIPT = join(REPO_ROOT, "scripts", "sync-plugin-version.mjs"); +const tempRoots: string[] = []; + +function readJson(path: string): T { + return JSON.parse(readFileSync(path, "utf8")) as T; +} + +function fixture(): string { + const root = mkdtempSync(join(tmpdir(), "spe-plugin-version-")); + tempRoots.push(root); + for (const file of ["package.json", "plugin.json", "mcp.json", "server.json"]) { + cpSync(join(REPO_ROOT, file), join(root, file)); + } + writeFileSync( + join(root, "package-lock.json"), + `${JSON.stringify({ + name: "@microsoft/spe-mcp", + version: "0.2.0-alpha.1", + lockfileVersion: 3, + packages: { + "": { name: "@microsoft/spe-mcp", version: "0.2.0-alpha.1" }, + }, + }, null, 2)}\n`, + ); + return root; +} + +afterEach(() => { + for (const root of tempRoots.splice(0)) { + rmSync(root, { recursive: true, force: true }); + } +}); + +describe("plugin release version synchronization", () => { + it("stages every manifest changed by the npm version lifecycle", () => { + const pkg = readJson<{ scripts: { version: string } }>(join(REPO_ROOT, "package.json")); + for (const file of ["plugin.json", "mcp.json", "server.json", "package-lock.json"]) { + expect(pkg.scripts.version.split(/\s+/)).toContain(file); + } + }); + + it("keeps the checked-in package and plugin versions synchronized", () => { + execFileSync(process.execPath, [SCRIPT, "--check"], { cwd: REPO_ROOT }); + }); + + it("stamps plugin.json and the exact MCP package pin from package.json", () => { + const root = fixture(); + const packagePath = join(root, "package.json"); + const pkg = readJson>(packagePath); + pkg.version = "9.8.7-alpha.6"; + writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`); + + execFileSync(process.execPath, [SCRIPT, "--root", root]); + + const plugin = readJson<{ version: string }>(join(root, "plugin.json")); + const lock = readJson<{ + version: string; + packages: { "": { version: string } }; + }>(join(root, "package-lock.json")); + const mcp = readJson<{ + mcpServers: { "sharepoint-embedded": { args: string[] } }; + }>(join(root, "mcp.json")); + const registry = readJson<{ + version: string; + packages: Array<{ version: string }>; + }>(join(root, "server.json")); + expect(plugin.version).toBe("9.8.7-alpha.6"); + expect(lock.version).toBe("9.8.7-alpha.6"); + expect(lock.packages[""].version).toBe("9.8.7-alpha.6"); + expect(mcp.mcpServers["sharepoint-embedded"].args).toContain( + "@microsoft/spe-mcp@9.8.7-alpha.6", + ); + expect(registry.version).toBe("9.8.7-alpha.6"); + expect(registry.packages.every((entry) => entry.version === "9.8.7-alpha.6")).toBe(true); + }); + + it("fails the prepack check when any plugin version drifts", () => { + const root = fixture(); + const pluginPath = join(root, "plugin.json"); + const plugin = readJson>(pluginPath); + plugin.version = "0.0.0"; + writeFileSync(pluginPath, `${JSON.stringify(plugin, null, 2)}\n`); + + const result = spawnSync(process.execPath, [SCRIPT, "--root", root, "--check"]); + expect(result.status).not.toBe(0); + expect(result.stderr.toString()).toContain("Plugin version drift"); + }); + + it("fails the prepack check when either package-lock version drifts", () => { + const root = fixture(); + const lockPath = join(root, "package-lock.json"); + const lock = readJson<{ + version: string; + packages: { "": { version: string } }; + }>(lockPath); + lock.packages[""].version = "0.0.0"; + writeFileSync(lockPath, `${JSON.stringify(lock, null, 2)}\n`); + + const result = spawnSync(process.execPath, [SCRIPT, "--root", root, "--check"]); + expect(result.status).not.toBe(0); + expect(result.stderr.toString()).toContain("package-lock.json"); + }); +});