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");
+ });
+});