Add support to publish servers as MCP Bundles (#1681)

* Testing MCPB detached signing with ESRP

* Updated signing config

* Updated the pipeline to attach signature to .mcpb file and verify

* Fixed file extension

* Created design document

* Created script to pack MCP bundles and updated docs

* Removed test .mcpb file

* Updated packing scrip to update server version on manifest

* Reverted changes to common.yml

* Created other scripts to be used with ESRP signing

* Added unit tests

* Implemented phase 2

* Implemented phase 3

* Updated docs

* Added a manifest.json and server icon for the Template server

* Added a manifest.json and server icon for the Fabric MCP server

* Updated manifests for all server

* Updated eng docs and build.yml

* Updated .gitignore

* Updated release-mcpb.yml to use inline PowerShell

* Updated docs

* Refactor MCPB packaging pipeline: remove Windows pool specification and update .NET SDK usage

* Updated docs to clarify the correct signature format

* Attempt to fix .NET version issue with mcpb CLI

* Trying to fix issue with applying signature

* Added MCBP flag to Fabric build.yml

* Fix uploading MCPB files to GH release.

* Add internalModuleProxy feature flag for golang in 1es-redirect.yml

* Removed old test artifacts

* Added changelog entry

* Updated server.json for Azure and Template servers

* Removed unsupported arguments from server.json files

* Updated server.json scripts to support missing platforms

* Updated, renamed, and relocated design doc

* Applied Copilot feedback

* Updated version in Azure MCP Server manifest

* Fixed broken links

* Actually fixed broken links

* Applied some PR feedback

* Modified scripts to use build_info.json files for mapping MCPB builds

* Polished up server.json approach and updated .csproj files for the Azure and Template servers

* Removed unnecessary server icons

* Removed other usages of Write-Host in favor of using LogInfo

* Added a centralized .config/dotnet-tools.json file to pin the mcpb CLI version

* Added workaround for Claude Desktop

* Updated packing script to use backslashes for Windows bundles

* Added missing PackagePyPI parameter to Template server config
This commit is contained in:
vcolin7 authored and GitHub committed 2026-02-20 13:07:06 -08:00
1 parent 00c70f7a8e
commit a1a6747cfe
27 files changed
+2078 -4

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"mcpb.cli": {
"version": "0.3.5",
"commands": [
"mcpb"
]
}
}
}
+1
View File
@@ -24,6 +24,7 @@ generated/
/docs/commandline
.DS_Store
.vscode/mcp.json
.vscode/settings.json
eng/tools/ToolDescriptionEvaluator/prompts.json
eng/tools/ToolDescriptionEvaluator/results.md
@@ -0,0 +1,657 @@
# Design Document: MCPB Packaging and Signing via ESRP
## Problem Statement
Microsoft's internal signing service (ESRP) does not currently support MCPB files directly. However, ESRP supports **generic PKCS#7 detached signing** (`Pkcs7DetachedSign` operation), which produces a standalone `.p7s` signature file that can be appended to any file format. Since MCPB uses PKCS#7 signatures, we can use ESRP's detached signing capability to sign MCPB files and convert the resulting signature to MCPB format.
Additionally, we need to package our MCP servers into the MCPB format using the official MCPB CLI tool. For best compatibility and performance, we should use the **trimmed version** of the server produced by the release pipeline, as it is as lightweight as we can make the server today and includes the .NET runtime.
---
## Goals
1. **Package MCP servers into MCPB format** using the official MCPB CLI tool
2. **Use trimmed server binaries** for lightweight and self-contained packages
3. **Enable MCPB signing** for all MCP servers in this repository using existing ESRP infrastructure
4. **Automate the entire process** as part of the release pipeline
5. **Support multiple servers** (Azure.Mcp.Server, Fabric.Mcp.Server, Template.Mcp.Server, etc.)
6. **Maintain compatibility** with the official MCPB signature format per the [mcpb CLI specification](https://github.com/modelcontextprotocol/mcpb/blob/main/CLI.md)
7. **Provide verification** that the signed MCPB files are valid using `mcpb verify`
## Non-Goals
1. Modifying ESRP to natively support MCPB (out of scope - requires Microsoft-wide coordination)
2. Creating a custom signing infrastructure
3. Supporting non-Microsoft signing services
---
## Background
### MCPB Format Overview
MCP Bundles (`.mcpb`) are ZIP archives containing a local MCP server and a `manifest.json` that describes the server and its capabilities. The format is similar to Chrome extensions (`.crx`) or VS Code extensions (`.vsix`), enabling end users to install local MCP servers with a single click.
**MCPB CLI Installation:**
```bash
# Restore from local tool manifest (recommended for our pipeline, version pinned in .config/dotnet-tools.json)
dotnet tool restore
# Or via npm
npm install -g @anthropic-ai/mcpb
```
**Binary Bundle Structure:**
```
bundle.mcpb (ZIP file)
├── manifest.json # Required: Bundle metadata and configuration
├── server/ # Server files
│ ├── azmcp # Unix executable
│ ├── azmcp.exe # Windows executable
│ └── [dependencies] # All required DLLs and resources
└── servericon.png # Bundle icon
```
### Why Trimmed Binaries?
For optimal performance with AI clients like Claude Desktop, we use **trimmed server binaries** produced by the release pipeline:
| Aspect | Full Build | Trimmed Build |
|--------|-----------|---------------|
| Size | ~150+ MB | ~100 MB |
| Startup Time | Slower | Faster |
| Dependencies | All included | Only used code |
| Client Experience | Standard | Lightweight |
The trimmed version removes unused code and dependencies through .NET's IL trimming, resulting in faster startup times critical for responsive AI client interactions.
### ESRP Pkcs7DetachedSign Operation
ESRP's `Pkcs7DetachedSign` operation creates a detached PKCS#7 signature for any file:
| Aspect | Details |
|--------|---------|
| Operation Code | `Pkcs7DetachedSign` |
| Key Code | `CP-230012` (Microsoft Corporation certificate) |
| Input | Any file (`.mcpb`, `.zip`, etc.) |
| Output | Detached `.p7s` signature file |
| Signature Type | DER-encoded PKCS#7/CMS |
The detached signature signs the entire file content and can be converted to MCPB's embedded signature format.
### MCPB Signature Structure
Per the [mcpb CLI documentation](https://github.com/modelcontextprotocol/mcpb/blob/main/CLI.md#signature-format), MCPB uses PKCS#7 (Cryptographic Message Syntax) for digital signatures:
```
[Original MCPB ZIP content]
MCPB_SIG_V1
[4-byte little-endian length prefix]
[DER-encoded PKCS#7 signature]
MCPB_SIG_END
```
> **Note on documentation vs implementation**: The official CLI.md documentation shows a simplified format with Base64-encoded signatures and no length prefix. However, we verified the actual source code for both the [NPM/TypeScript implementation](https://github.com/modelcontextprotocol/mcpb/blob/main/src/node/sign.ts) and the [C#/.NET implementation](https://github.com/asklar/mcpb/blob/user/asklar/dotnet/dotnet/mcpb/Commands/SignCommand.cs) - both use **DER-encoded (raw binary) signatures with a 4-byte little-endian length prefix**, not Base64. In TypeScript, `createSignatureBlock` writes `writeUInt32LE(pkcs7Signature.length)` and `extractSignatureBlock` reads with `readUInt32LE`. In C#, `CreateSignatureBlock` uses `BitConverter.GetBytes(pkcs7.Length)` and `ExtractSignatureBlock` reads with `BitConverter.ToInt32`. This length prefix is essential for unambiguous parsing, as raw DER bytes could otherwise contain sequences that match the `MCPB_SIG_END` marker.
This approach allows:
- Backward compatibility (unsigned MCPB files are valid ZIP files)
- Easy signature verification and removal
- Support for certificate chains with intermediate certificates
### ZIP Compatibility (EOCD Comment Length)
Standard ZIP parsers locate the End of Central Directory (EOCD) record and expect the file to end at `EOCD_offset + 22 + comment_length`. When the MCPB signature block is naively appended, the EOCD `comment_length` remains 0, but extra bytes (the signature block) follow — causing strict ZIP parsers to reject the file.
**Affected tools:** Claude Desktop uses a strict ZIP parser that rejects files with unexpected trailing bytes:
```
Failed to read or unzip file: Invalid comment length. Expected: 3997. Found: 0.
```
**Our fix:** Before ESRP signing, we set the EOCD `comment_length` to a fixed constant (`MAX_SIG_BLOCK_SIZE = 16384`). After signing, we pad the signature block with zero bytes to exactly that size. The result is a valid ZIP where the "comment" bytes happen to be the MCPB signature block.
```
[ZIP entries + Central Directory + EOCD (comment_length=16384)]
[MCPB_SIG_V1][4-byte len][DER signature][zero padding][MCPB_SIG_END]
|<-------- exactly 16384 bytes = EOCD comment_length ----------->|
```
**Why this works end-to-end:**
| Step | Behavior |
|------|----------|
| ESRP signing | Signs the MCPB *after* EOCD is updated, so the hash covers `comment_length=16384` |
| `mcpb verify` | Extracts `originalContent` = everything before `MCPB_SIG_V1` = the EOCD-modified file = what ESRP signed ✅ |
| `mcpb unpack` | Strips signature, reads ZIP entries normally ✅ |
| ZIP parsers | See `comment_length=16384` and 16384 bytes follow EOCD ✅ |
| Claude Desktop | Treats it as a valid ZIP and installs successfully ✅ |
### ESRP Detached Signing Output
- Input: `file.mcpb` (staged using the `.signature.p7s` extension for ESRP processing, with EOCD comment_length pre-set)
- Output: `file.signature.p7s` (detached PKCS#7/CMS signature)
The signature signs the entire file content (including the pre-set EOCD comment_length). ESRP replaces the staged file with the signature, so we stage a copy with a `.signature.p7s` extension and keep the original `.mcpb` intact.
---
## Proposed Solution
### High-Level Workflow
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ MCPB Packaging and Signing Workflow │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 1. Build Phase (existing pipeline) │
│ ┌──────────────────────┐ │
│ │ dotnet publish │ │
│ │ (trimmed binaries) │ ──────► /publish/win-x64/ │
│ └──────────────────────┘ /publish/linux-x64/ │
│ /publish/osx-x64/ │
│ /publish/osx-arm64/ │
│ │
│ 2. Prepare MCPB Structure │
│ ┌────────────────────────┐ │
│ │ Copy manifest.json │ │
│ │ Bundle server binaries │ ──────► /mcpb-staging/{platform}/ │
│ │ Copy icon │ ├── manifest.json │
│ └────────────────────────┘ ├── server/ │
│ │ └── [trimmed binaries] │
│ └── servericon.png │
│ │
│ 3. Package with MCPB CLI │
│ ┌──────────────────────────────┐ │
│ │ mcpb validate │ │
│ │ mcpb pack (updates manifest) │ ──────► {ServerName}-{platform}.mcpb │
│ └──────────────────────────────┘ (unsigned) │
│ │
│ 4. ESRP Detached Signing (Pkcs7DetachedSign) │
│ ┌──────────────────────┐ ┌──────────────────┐ │
│ │ unsigned.mcpb │ ──ESRP──► signature.p7s │ │
│ │ (Pkcs7DetachedSign) │ │ (detached sig) │ │
│ └──────────────────────┘ └──────────────────┘ │
│ │
│ 5. Convert & Apply Signature │
│ ┌──────────────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ unsigned.mcpb │ +│ signature.p7s│ ───► signed.mcpb │ │
│ └──────────────────────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────┐ │
│ │ MCPB_SIG_V1 │ │
│ │ [length][signature] │ │
│ │ MCPB_SIG_END │ │
│ └────────────────────────┘ │
│ │
│ 6. Verification │
│ ┌──────────────────────┐ │
│ │ mcpb verify │ ──────► ✓ Valid signature │
│ │ mcpb info │ │
│ └──────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### Component Design
#### 1. MCPB CLI Installation
The MCPB CLI is required for packaging and verification. Restore via local tool manifest (version pinned in `.config/dotnet-tools.json`):
```bash
dotnet tool restore
```
The CLI provides essential commands:
- `mcpb init` - Create manifest.json interactively
- `mcpb validate` - Validate manifest against schema
- `mcpb pack` - Package directory into .mcpb file. Updates the manifest to include latest tool info.
- `mcpb sign` - Sign with certificate (not used - we use ESRP)
- `mcpb verify` - Verify signature of signed .mcpb
- `mcpb info` - Display information about .mcpb file
#### 2. PowerShell Script: `Pack-Mcpb.ps1`
Location: `eng/scripts/Pack-Mcpb.ps1`
**Parameters:**
- `-ArtifactsPath`: Path to the build artifacts directory containing trimmed server binaries
- `-BuildInfoPath`: Path to the `build_info.json` file containing server and platform definitions
- `-OutputPath`: Output directory for `.mcpb` files
- `-KeepStagingDirectory`: Optional flag to keep staging directory for debugging
**Supported Platforms** (determined by `build_info.json`):
- `win-x64`, `win-arm64` (Windows)
- `linux-x64`, `linux-arm64` (Linux)
- `osx-x64`, `osx-arm64` (macOS)
**Workflow:**
1. Create staging directory structure
2. Copy trimmed binaries to `server/` subdirectory
3. Copy `manifest.json` (uses `platform_overrides` in mcp_config for cross-platform support)
4. Copy icon and assets. Rename icon to `servericon.png`.
5. Copy LICENSE and NOTICE.txt into each bundle
6. Validate with `mcpb validate`
7. Package with `mcpb pack --update` (auto-populates tools array)
#### 3. Modular Pipeline Scripts
The pipeline uses separate scripts for each signing phase, enabling better maintainability and testability:
**`Stage-McpbForSigning.ps1`** - Location: `eng/scripts/Stage-McpbForSigning.ps1`
- Creates copies of `.mcpb` files with `.signature.p7s` extension for ESRP processing
- **Updates the ZIP EOCD comment length** to `MAX_SIG_BLOCK_SIZE` (16384) before staging, so that the signed file remains a valid ZIP (see [ZIP Compatibility](#zip-compatibility-eocd-comment-length))
- Parameters: `-ArtifactsPath`, `-StagingPath`
**`Apply-McpbSignatures.ps1`** - Location: `eng/scripts/Apply-McpbSignatures.ps1`
- Applies ESRP-generated `.p7s` signatures to MCPB files
- Contains internal `Convert-P7sToMcpbSignature` function for signature format conversion
- Wraps .p7s in MCPB signature format (MCPB_SIG_V1 + length + sig + **padding** + MCPB_SIG_END)
- **Pads the signature block** with zero bytes so the total block size equals `MAX_SIG_BLOCK_SIZE`, matching the pre-set EOCD comment length
- Parameters: `-ArtifactsPath`, `-OutputPath`
**`Verify-McpbSignatures.ps1`** - Location: `eng/scripts/Verify-McpbSignatures.ps1`
- Verifies all signed MCPB files using `mcpb verify`
- Fails pipeline if any signature verification fails
- Parameters: `-ArtifactsPath`
#### 4. Signature Conversion Logic
```
Input: signature.p7s (DER-encoded PKCS#7)
Output: MCPB signature block (padded to MAX_SIG_BLOCK_SIZE)
Algorithm:
1. Read .p7s file as bytes
2. Calculate length (4-byte little-endian)
3. Calculate padding = MAX_SIG_BLOCK_SIZE - (11 + 4 + sig_length + 12)
4. Construct signature block:
- "MCPB_SIG_V1" (11 bytes, ASCII)
- Length prefix (4 bytes, little-endian) — actual signature length, NOT padded
- Signature bytes (from .p7s)
- Zero padding (padding bytes)
- "MCPB_SIG_END" (12 bytes, ASCII)
5. Append to original .mcpb content (which has EOCD comment_length = MAX_SIG_BLOCK_SIZE)
```
> The padding ensures the total appended block size equals the EOCD comment length,
> making the file a valid ZIP. The `mcpb` extraction logic reads exactly `sig_length`
> bytes via the 4-byte length prefix and ignores the padding.
#### 5. Pipeline Integration
Location: `eng/pipelines/templates/jobs/mcpb/pack-and-sign-mcpb.yml`
**Template parameters:**
- `DependsOn`: Jobs that must complete before packaging (typically `Sign`)
**Steps:**
1. Install MCPB CLI tool
2. Download signed binaries and build_info.json
3. Package all servers for all platforms using `Pack-Mcpb.ps1`
4. Stage files for ESRP signing using `Stage-McpbForSigning.ps1`
5. Submit to ESRP using Pkcs7DetachedSign operation
6. Apply signatures using `Apply-McpbSignatures.ps1`
7. Verify using `Verify-McpbSignatures.ps1`
8. Publish signed .mcpb artifacts to GitHub using inline PowerShell in `release-mcpb.yml`
---
## Implementation Details
### File Structure
```
eng/
├── scripts/
│ ├── Pack-Mcpb.ps1 # MCPB packaging script (uses build_info.json)
│ ├── Stage-McpbForSigning.ps1 # Stage files for ESRP signing
│ ├── Apply-McpbSignatures.ps1 # Apply .p7s signatures (includes Convert-P7sToMcpbSignature)
│ ├── Verify-McpbSignatures.ps1 # Verify signatures using mcpb CLI
│ └── Update-ServerJsonMcpbHashes.ps1 # Compute SHA256 for MCP Registry
├── pipelines/
│ └── templates/
│ └── jobs/
│ └── mcpb/
│ ├── pack-and-sign-mcpb.yml # Pack and sign pipeline template
│ └── release-mcpb.yml # Release pipeline template
docs/
└── mcpb-packaging-and-signing-via-esrp.md # This design document
servers/
├── Azure.Mcp.Server/
│ ├── mcpb/
│ │ ├── manifest.json # MCPB manifest
│ │ └── servericon.png # Server icon
│ └── server.json # MCP Registry config with MCPB entries
├── Fabric.Mcp.Server/
│ ├── mcpb/
│ │ ├── manifest.json # MCPB manifest
│ │ └── servericon.png # Server icon
│ └── server.json # MCP Registry config with MCPB entries
└── Template.Mcp.Server/
├── mcpb/
│ ├── manifest.json # MCPB manifest
│ └── servericon.png # Server icon
└── server.json # MCP Registry config with MCPB entries
```
### Integration with Build Info
The MCPB packaging scripts integrate with the existing `build_info.json` infrastructure, which is the source of truth for all packaging scripts. This provides:
- **Consistent versioning** across all package formats (npm, NuGet, VSIX, MCPB)
- **Platform discovery** from the build matrix
- **Server metadata** (name, cliName, description, icon path)
- **Artifact paths** for locating trimmed binaries
The `build_info.json` file is created by `New-BuildInfo.ps1` and consumed by all `Pack-*.ps1` scripts.
### Manifest Requirements
Each server must have a complete `manifest.json` file in `servers/{ServerName}/mcpb/`. The manifest should:
1. **Use `platform_overrides`** in `mcp_config` to handle cross-platform executable paths (Windows `.exe` vs Unix)
2. **Include all metadata** (name, version, description, author, etc.)
3. **Reference the correct icon** path relative to the manifest location
The `mcpb pack` command will automatically populate the `tools` array from the server's MCP tool definitions during packaging.
**Important Path Handling:**
- **`entry_point`**: Must be a relative path (e.g., `server/azmcp.exe`) without `${__dirname}`. The `mcpb pack` command verifies this file exists in the staging directory.
- **`mcp_config.command`**: Should use `${__dirname}` prefix (e.g., `${__dirname}/server/azmcp.exe`) for runtime path resolution. Both dotnet and npm mcpb validators accept this.
The `Pack-Mcpb.ps1` script automatically adds the platform-specific extension (`.exe` for Windows) to both paths.
**Important Manifest Version:** The manifest must use `"manifest_version": "0.3"` or higher to support the `_meta` field that `mcpb pack --update` adds. Using `0.2` will cause Claude Desktop to reject the bundle with "Unrecognized key(s) in object: '_meta'".
**Example:** See [servers/Azure.Mcp.Server/mcpb/manifest.json](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/servers/Azure.Mcp.Server/mcpb/manifest.json) for a complete manifest.
### Script: `Pack-Mcpb.ps1`
Location: [`eng/scripts/Pack-Mcpb.ps1`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/scripts/Pack-Mcpb.ps1)
This is the main packaging script that follows the same patterns as `Pack-Npm.ps1`, `Pack-Vsix.ps1`, etc.
**Key behaviors:**
- Reads server list and platform definitions from `build_info.json`
- Filters to trimmed, non-native, non-special-purpose platforms
- Creates `.mcpb` files named `{ServerName}-{os}-{arch}.mcpb` (e.g., `Azure.Mcp.Server-win-x64.mcpb`)
- Validates manifest with `mcpb validate` before packaging
- Copies LICENSE and NOTICE.txt into each bundle
### Script: `Apply-McpbSignatures.ps1`
Location: [`eng/scripts/Apply-McpbSignatures.ps1`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/scripts/Apply-McpbSignatures.ps1)
This script applies ESRP-generated `.p7s` signatures to MCPB files. It contains the `Convert-P7sToMcpbSignature` function internally for signature format conversion.
**Key behaviors:**
- Finds all `.mcpb` files and their corresponding `.signature.p7s` files
- Converts PKCS#7 signatures to MCPB embedded format
- Preserves directory structure in output
- Reports success/failure counts
### Pipeline Template: `pack-and-sign-mcpb.yml`
Location: [`eng/pipelines/templates/jobs/mcpb/pack-and-sign-mcpb.yml`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/pipelines/templates/jobs/mcpb/pack-and-sign-mcpb.yml)
This template orchestrates the packaging and signing workflow:
1. **PackMcpb job**: Installs MCPB CLI, downloads trimmed binaries, packages all servers
2. **SignMcpb job**: Stages files, submits to ESRP, applies signatures, verifies with `mcpb verify`
**Pipeline artifacts:**
- `mcpb-unsigned`: Unsigned MCPB packages (intermediate)
- `mcpb-signed`: Signed MCPB packages (final output)
### Pipeline Template: `release-mcpb.yml`
Location: [`eng/pipelines/templates/jobs/mcpb/release-mcpb.yml`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/pipelines/templates/jobs/mcpb/release-mcpb.yml)
This template handles publishing signed MCPB packages to GitHub Release. The upload logic is inlined directly in the YAML because 1ES release jobs (`templateContext.type: releaseJob`) do not allow `checkout` steps.
---
## Server Support Matrix
| Server | Path | Notes |
|--------|------|-------|
| Azure.Mcp.Server | `servers/Azure.Mcp.Server/` | Azure server | ✅ manifest.json created |
| Fabric.Mcp.Server | `servers/Fabric.Mcp.Server/` | Fabric server | ✅ manifest.json created |
| Template.Mcp.Server | `servers/Template.Mcp.Server/` | Template for new servers | ✅ manifest.json created |
All servers have complete `manifest.json` files with:
- `manifest_version: "0.3"` for Claude Desktop compatibility
- `platform_overrides` for cross-platform executable paths
- Proper `entry_point` and `mcp_config.command` paths
- Server icon (`servericon.png`) in the mcpb directory
The signing script auto-discovers servers based on the `servers/` directory structure and the presence of `manifest.json` files.
---
## Testing Strategy
### Unit Tests
1. **Signature format validation**: Verify the generated signature block matches MCPB spec
2. **Length prefix correctness**: Test with various signature sizes
3. **Binary integrity**: Ensure no corruption during file operations
### Integration Tests
1. **Round-trip test**: Sign → Verify → Unsign → Compare with original
2. **mcpb CLI compatibility**: Verify signed files work with `mcpb verify`, `mcpb info`
3. **Cross-platform**: Test on Windows, Linux, macOS
### Pipeline Tests
1. **Dry-run mode**: Test pipeline without actual ESRP submission
2. **Mock .p7s files**: Use pre-generated signatures for testing
---
## Risks and Mitigations
| Risk | Impact | Mitigation |
|------|--------|------------|
| Signature format changes in mcpb spec | Medium | Version the signature format; monitor mcpb CLI updates |
| ESRP rate limiting during batch signing | Low | Implement retry logic with exponential backoff |
| Certificate expiration | Medium | Monitor cert validity; integrate with cert renewal alerts |
| Trimmed binaries missing dependencies | Medium | Test thoroughly on clean systems; include all required native libraries |
| MCPB CLI version incompatibility | Low | Pin CLI version; test updates in staging before production |
| ESRP replaces file with signature | Low | Use staging pattern: copy to `.signature.p7s` before signing |
---
## Open Questions
1. **Should we also support local/self-signed certificates for development?**
- Recommendation: Yes, use `mcpb sign --self-signed` for local testing
2. **How to handle signature verification failures in the pipeline?**
- Recommendation: Fail the build immediately; do not publish unsigned artifacts
3. **Should we store the .p7s files separately as artifacts?**
- Recommendation: Yes, for audit and debugging purposes
4. **How should the manifest.json tools array be populated?**
- Recommendation: Generate dynamically from the server's tool definitions via the `mpcb pack` command
5. **Should we support per-platform manifest variations?**
- Recommendation: Use a single manifest with platform_overrides in mcp_config for command paths
---
## Implementation Phases
### Phase 1: Core Scripts and Templates (Week 1) ✅
- [x] Implement `Pack-Mcpb.ps1` for packaging (integrated with build_info.json)
- [x] Implement modular pipeline scripts:
- [x] `Stage-McpbForSigning.ps1` - Stage files for ESRP
- [x] `Apply-McpbSignatures.ps1` - Apply .p7s signatures (includes `Convert-P7sToMcpbSignature` function)
- [x] `Verify-McpbSignatures.ps1` - Verify using mcpb CLI
- [x] Verify manifest.json for Azure.Mcp.Server (uses manifest_version 0.3)
- [x] Add unit tests for signature conversion (`Apply-McpbSignatures.tests.ps1`)
- [x] Document local usage
- [x] Validate MCPB bundle works in Claude Desktop
### Phase 2: Pipeline Integration (Week 2) ✅
- [x] Create `pack-and-sign-mcpb.yml` pipeline template
- [x] Create `release-mcpb.yml` pipeline template
- [x] Integrate with existing release pipeline (`common.yml`, `sign-and-pack.yml`, `release.yml`)
- [x] Enable MCPB packaging for Azure.Mcp.Server (`build.yml`)
- [x] Test end-to-end with Azure.Mcp.Server (requires pipeline run)
- [x] Verify signatures with `mcpb verify` (requires pipeline run)
### Phase 3: Multi-Server Support and MCP Registry (Week 3) ✅
- [x] Add MCPB package entries to `server.json` with platform-specific URLs and SHA256 placeholders
- [x] Update `New-ServerJson.ps1` to populate MCPB package URLs from GitHub Release
- [x] Create `Update-ServerJsonMcpbHashes.ps1` to compute SHA256 hashes after signing
- [x] Update `update-mcp-repository.yml` to compute hashes before publishing to MCP Registry
- [x] Create manifest.json for Fabric.Mcp.Server
- [x] Create manifest.json for Template.Mcp.Server
- [ ] Test MCP Registry publishing end-to-end (requires pipeline run)
---
## MCP Registry Publishing
The MCP Registry requires MCPB packages to include:
1. A GitHub Release URL (`identifier`) pointing to the `.mcpb` file
2. A SHA256 hash (`fileSha256`) for file integrity verification
### Supported Platforms
Each server is packaged for **6 platforms**:
| Platform | OS | Architecture | MCPB Filename Suffix |
|----------|----|--------------|-----------------------|
| `win-x64` | Windows | x86_64 | `-win-x64.mcpb` |
| `win-arm64` | Windows | arm64 | `-win-arm64.mcpb` |
| `linux-x64` | Linux | x86_64 | `-linux-x64.mcpb` |
| `linux-arm64` | Linux | arm64 | `-linux-arm64.mcpb` |
| `osx-x64` | macOS | x86_64 | `-osx-x64.mcpb` |
| `osx-arm64` | macOS | arm64 | `-osx-arm64.mcpb` |
### server.json MCPB Entries
Each platform has its own MCPB package entry in `server.json`. The placeholders (`<<McpbUrl*>>` and `<<McpbSha256*>>`) are replaced at release time:
```json
{
"registryType": "mcpb",
"identifier": "https://github.com/microsoft/mcp/releases/download/Azure.Mcp.Server-1.0.0/Azure.Mcp.Server-win-x64.mcpb",
"fileSha256": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce",
"transport": {
"type": "stdio"
}
}
```
### SHA256 Hash Computation
The `Update-ServerJsonMcpbHashes.ps1` script computes SHA256 hashes for signed MCPB files:
```powershell
# Compute hash using PowerShell
(Get-FileHash -Path "Azure.Mcp.Server-win-x64.mcpb" -Algorithm SHA256).Hash.ToLower()
# Or using openssl
openssl dgst -sha256 Azure.Mcp.Server-win-x64.mcpb
```
### Pipeline Flow
```
PackMcpb → SignMcpb → PublishMcpb (GitHub Release) → UpdateMcpRepository
↓
Download MCPB artifacts
↓
Compute SHA256 hashes
↓
Update server.json
↓
Publish to MCP Registry
```
---
## Local Development and Testing
### Using Pack-Mcpb.ps1 (Recommended)
The easiest way to create MCPB packages locally is to use the same script the pipeline uses:
```powershell
# Step 1: Build trimmed server binaries
./eng/scripts/Build-Code.ps1 -ServerName "Azure.Mcp.Server" -SelfContained -ReleaseBuild -Trimmed
# Step 2: Generate build_info.json
./eng/scripts/New-BuildInfo.ps1 -ServerName "Azure.Mcp.Server"
# Step 3: Create MCPB packages for all platforms
./eng/scripts/Pack-Mcpb.ps1
# Output will be in .work/packages_mcpb/
```
Or with custom paths:
```powershell
./eng/scripts/Pack-Mcpb.ps1 `
-ArtifactsPath ".work/build" `
-BuildInfoPath ".work/build_info.json" `
-OutputPath "./artifacts/mcpb"
```
### Validate and Inspect Packages
```bash
# Show package information
mcpb info .work/packages_mcpb/Azure.Mcp.Server/Azure.Mcp.Server-win-x64.mcpb
# Validate manifest without packing
mcpb validate servers/Azure.Mcp.Server/mcpb/
```
### Self-Signed Testing
```bash
# Sign with self-signed certificate for local testing
mcpb sign .work/packages_mcpb/Azure.Mcp.Server/Azure.Mcp.Server-win-x64.mcpb --self-signed
# Verify (will show self-signed warning)
mcpb verify .work/packages_mcpb/Azure.Mcp.Server/Azure.Mcp.Server-win-x64.mcpb
```
### Testing with Claude Desktop
1. Build and sign the MCPB package (self-signed for testing)
2. Double-click the `.mcpb` file to install in Claude Desktop
3. Verify the server appears in Claude's MCP server list
4. Test tool invocations
---
## References
- [MCPB Repository](https://github.com/modelcontextprotocol/mcpb)
- [MCPB Manifest Specification](https://github.com/modelcontextprotocol/mcpb/blob/main/MANIFEST.md)
- [MCPB CLI Documentation](https://github.com/modelcontextprotocol/mcpb/blob/main/CLI.md)
- [MCPB Signature Format Specification](https://github.com/modelcontextprotocol/mcpb/blob/main/CLI.md#signature-format)
- [PKCS#7 / CMS Standard (RFC 5652)](https://tools.ietf.org/html/rfc5652)
---
## Approval
- [ ] Security review (signing process)
- [ ] Pipeline team review
- [ ] Documentation review
---
**Author:** Azure MCP Team
**Date:** February 9, 2026
**Status:** Implementation Complete - Pending pipeline validation
+17 -1
View File
@@ -190,6 +190,22 @@ Servers are packed into two npm package types:
Zip files for each server in a release are added to the GitHub release assets.
### MCPB (MCP Bundle)
- packed with [`Pack-Mcpb.ps1`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/scripts/Pack-Mcpb.ps1)
- signed via [`pack-and-sign-mcpb.yml`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/pipelines/templates/jobs/mcpb/pack-and-sign-mcpb.yml)
- released via [`release-mcpb.yml`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/pipelines/templates/jobs/mcpb/release-mcpb.yml)
- requires a `manifest.json` and `servericon.png` under `servers/<ServerName>/mcpb/`
MCPB files are signed MCP bundles that can be installed directly into Claude Desktop with a single click. Each platform (win-x64, linux-x64, osx-x64, osx-arm64) produces a separate `.mcpb` file containing trimmed server binaries. The bundles are signed using ESRP's Pkcs7DetachedSign operation and verified with the `mcpb verify` CLI.
Signing pipeline scripts:
- [`Stage-McpbForSigning.ps1`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/scripts/Stage-McpbForSigning.ps1) - Prepares files for ESRP signing
- [`Apply-McpbSignatures.ps1`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/scripts/Apply-McpbSignatures.ps1) - Applies PKCS#7 signatures to MCPB files
- [`Verify-McpbSignatures.ps1`](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/eng/scripts/Verify-McpbSignatures.ps1) - Verifies signatures using mcpb CLI
For design details, see [docs/mcpb-packaging-and-signing-via-esrp.md](https://github.com/microsoft/mcp/blob/vcolin7/test-mcpb-signing/docs/design/mcpb-packaging-and-signing-via-esrp.md).
# Server release process
We only ship the repo's server projects, with each server stored under `servers/<ServerName>`. The release pipelines are driven by the server-specific `build.yml` files (for example, `servers/Azure.Mcp.Server/build.yml`). Those files opt a server into the artifact types we publish and pass the settings that flow through `/eng/pipelines/templates/common.yml`.
@@ -202,7 +218,7 @@ We only ship the repo's server projects, with each server stored under `servers/
- **Expect post-release automation**: After a successful public release, `jobs/release.yml` runs `Update-Version.ps1` to increment the server version and open a PR. This PR must be merged before the next release can start.
## Server release configuration
The parameters in a server's `build.yml` decide what we produce. Set `PackageDocker: true` to include Docker images, `PackageVSIX: true` to include VS Code extensions. Every pipeline always creates the npm/npx and NuGet/dnx artifacts.
The parameters in a server's `build.yml` decide what we produce. Set `PackageDocker: true` to include Docker images, `PackageVSIX: true` to include VS Code extensions, `PackageMCPB: true` to include signed MCP bundles. Every pipeline always creates the npm/npx and NuGet/dnx artifacts.
## Project file properties
Packaging and release scripts depend on metadata found in a build's build_info.json file, which is generated by `eng/scripts/New-BuildInfo.ps1` using properties from each server's project file. See [eng/scripts/Get-ProjectProperties.ps1](https://github.com/microsoft/mcp/blob/main/eng/scripts/Get-ProjectProperties.ps1) for a list of all supported project file properties.
+5
View File
@@ -21,6 +21,9 @@ parameters:
- name: PackagePyPI
type: boolean
default: false
- name: PackageMCPB
type: boolean
default: false
- name: TestTimeoutInMinutes
type: number
default: 30
@@ -122,6 +125,7 @@ extends:
PackageDocker: ${{ parameters.PackageDocker }}
PackageVSIX: ${{ parameters.PackageVSIX }}
PackagePyPI: ${{ parameters.PackagePyPI }}
PackageMCPB: ${{ parameters.PackageMCPB }}
- ${{ if eq(parameters.PublishTarget, 'public') }}:
- stage: Release
@@ -143,6 +147,7 @@ extends:
PackageDocker: ${{ parameters.PackageDocker }}
PackageVSIX: ${{ parameters.PackageVSIX }}
PackagePyPI: ${{ parameters.PackagePyPI }}
PackageMCPB: ${{ parameters.PackageMCPB }}
IsTestPipeline: ${{ parameters.IsTestPipeline }}
- ${{ elseif eq(parameters.PublishTarget, 'internal') }}:
@@ -0,0 +1,129 @@
# Pipeline template for packaging and signing MCPB bundles via ESRP
#
# This template:
# 1. Downloads signed server binaries and build_info.json
# 2. Packages the binaries into MCP bundles (.mcpb) using the MCPB CLI
# 3. Signs the bundles using ESRP's Pkcs7DetachedSign operation
# 4. Converts the detached .p7s signatures to MCPB embedded format
# 5. Verifies the signatures using mcpb verify
#
# Prerequisites:
# - Signed binaries artifact must exist (binaries_signed)
# - build_info.json must exist with server and platform definitions
# - The server must have a manifest.json in servers/{ServerName}/mcpb/
parameters:
- name: DependsOn
type: object
jobs:
- job: SignAndPackMcpb
displayName: "Sign and Pack MCPB"
dependsOn: ${{ parameters.DependsOn }}
condition: and(succeeded(), ne(variables['NoPackagesChanged'], 'true'))
steps:
- checkout: self
- download: current
displayName: "Download build_info"
artifact: build_info
- download: current
displayName: "Download binaries_signed"
artifact: binaries_signed
# Install .NET SDK 10.0.x (required by global.json for dotnet CLI commands)
- task: UseDotNet@2
displayName: "Install .NET SDK 10.0.x"
retryCountOnTaskFailure: 3
inputs:
packageType: sdk
version: 10.0.x
# Install .NET SDK 8.0.x (required by MCPB CLI which targets .NET 8.0)
- task: UseDotNet@2
displayName: "Install .NET SDK 8.0.x"
retryCountOnTaskFailure: 3
inputs:
packageType: sdk
version: 8.0.x
- pwsh: |
Write-Host "Restoring MCPB CLI from local tool manifest..."
dotnet tool restore
dotnet mcpb --version
displayName: "Restore MCPB CLI"
workingDirectory: $(Build.SourcesDirectory)
- task: Powershell@2
displayName: "Pack MCP bundles"
inputs:
pwsh: true
filePath: $(Build.SourcesDirectory)/eng/scripts/Pack-Mcpb.ps1
arguments: >
-BuildInfoPath '$(Pipeline.Workspace)/build_info/build_info.json'
-ArtifactsPath '$(Pipeline.Workspace)/binaries_signed'
-OutputPath '$(Build.ArtifactStagingDirectory)/mcpb'
- task: Powershell@2
displayName: "Stage MCPB files for signing"
inputs:
pwsh: true
filePath: $(Build.SourcesDirectory)/eng/scripts/Stage-McpbForSigning.ps1
arguments: >
-ArtifactsPath '$(Build.ArtifactStagingDirectory)/mcpb'
-OutputPath '$(Build.ArtifactStagingDirectory)/to_sign'
# Sign with ESRP using Pkcs7DetachedSign operation
# This produces detached PKCS#7 signatures that we'll embed in the MCPB format
- task: EsrpCodeSigning@6
displayName: "Sign MCPB - Detached PKCS#7"
inputs:
ConnectedServiceName: 'Azure SDK PME Managed Identity'
UseMSIAuthentication: true
AppRegistrationClientId: '4fa2001b-ad7a-4a1e-9185-d6ea881f8712'
AppRegistrationTenantId: '975f013f-7f24-47e8-a7d3-abc4752bf346'
EsrpClientId: '5e796b8d-3c4d-4e28-93f6-12a44f4368a0'
AuthAKVName: 'kv-azuresdk-codesign'
AuthSignCertName: 'azure-sdk-esrp-signing-certificate'
FolderPath: $(Build.ArtifactStagingDirectory)/to_sign
Pattern: '**/*.signature.p7s'
UseMinimatch: true
signConfigType: inlineSignParams
inlineOperation: |
[
{
"KeyCode": "CP-230012",
"OperationCode": "Pkcs7DetachedSign",
"ToolName": "sign",
"ToolVersion": "1.0",
"Parameters": {
"P7CE": "/p7ce DetachedSignedData",
"P7EKU": "1.2.840.113549.1.7.1",
"FileDigest": "/fd SHA256"
}
}
]
- task: Powershell@2
displayName: "Apply MCPB signatures"
inputs:
pwsh: true
filePath: $(Build.SourcesDirectory)/eng/scripts/Apply-McpbSignatures.ps1
arguments: >
-ArtifactsPath '$(Build.ArtifactStagingDirectory)/to_sign'
-OutputPath '$(Build.ArtifactStagingDirectory)/signed'
- task: Powershell@2
displayName: "Verify MCPB signatures"
inputs:
pwsh: true
filePath: $(Build.SourcesDirectory)/eng/scripts/Verify-McpbSignatures.ps1
arguments: >
-ArtifactsPath '$(Build.ArtifactStagingDirectory)/signed'
- template: /eng/common/pipelines/templates/steps/publish-1es-artifact.yml
parameters:
ArtifactPath: $(Build.ArtifactStagingDirectory)/signed
ArtifactName: packages_mcpb_signed
SbomEnabled: ${{ ne(variables['Build.Reason'], 'PullRequest') }}
@@ -0,0 +1,111 @@
# Pipeline template for releasing signed MCPB bundles
#
# This template:
# 1. Downloads signed MCPB bundles
# 2. Uploads them to the GitHub Release created by TagRepository job
#
# Prerequisites:
# - packages_mcpb_signed artifact must exist
# - build_info.json must exist with server and version information
# - GitHub Release must already be created (by TagRepository job)
parameters:
- name: ServerName
type: string
- name: DependsOn
type: object
jobs:
- deployment: PublishMCPB
displayName: "Publish MCPB to GitHub Release"
dependsOn: ${{ parameters.DependsOn }}
condition: and(succeeded(), ne(variables['Skip.PublishPackage'], 'true'))
templateContext:
type: releaseJob
isProduction: true
inputs:
- input: pipelineArtifact
artifactName: packages_mcpb_signed
targetPath: $(Pipeline.Workspace)/mcpb
- input: pipelineArtifact
artifactName: build_info
targetPath: $(Pipeline.Workspace)/build_info
environment: package-publish
pool:
name: azsdk-pool
image: ubuntu-24.04
os: linux
strategy:
runOnce:
deploy:
steps:
# Note: checkout is not allowed in release jobs, so we inline the script logic here
- pwsh: |
$ErrorActionPreference = "Stop"
$serverName = '${{ parameters.ServerName }}'
$mcpbPath = '$(Pipeline.Workspace)/mcpb'
$buildInfoPath = '$(Pipeline.Workspace)/build_info/build_info.json'
Write-Host "Loading build info from $buildInfoPath..."
$buildInfo = Get-Content -Raw -Path $buildInfoPath | ConvertFrom-Json
$server = $buildInfo.servers | Where-Object { $_.name -eq $serverName } | Select-Object -First 1
if (-not $server) {
Write-Error "Server '$serverName' not found in build info."
exit 1
}
$version = $server.version
$tag = $server.releaseTag
Write-Host "Server: $serverName"
Write-Host "Version: $version"
Write-Host "Release Tag: $tag"
# Find MCPB files for this server
$serverMcpbPath = Join-Path $mcpbPath $serverName
if (-not (Test-Path $serverMcpbPath)) {
Write-Error "No MCPB files found for $serverName at $serverMcpbPath"
exit 1
}
$mcpbFiles = Get-ChildItem -Path $serverMcpbPath -Filter "*.mcpb" -Recurse
if ($mcpbFiles.Count -eq 0) {
Write-Error "No .mcpb files found in $serverMcpbPath"
exit 1
}
Write-Host "`nUploading $($mcpbFiles.Count) MCPB file(s) to release $tag..."
$uploadedCount = 0
$failedCount = 0
foreach ($mcpb in $mcpbFiles) {
Write-Host " Uploading: $($mcpb.Name) ($($mcpb.Length) bytes)"
# Note: --repo is required because checkout is not allowed in release jobs,
# so gh cannot determine the repository from the git context
& gh release upload $tag $mcpb.FullName --repo microsoft/mcp --clobber
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to upload $($mcpb.Name)"
$failedCount++
} else {
$uploadedCount++
}
}
Write-Host "`nUpload Summary:"
Write-Host " Uploaded: $uploadedCount"
Write-Host " Failed: $failedCount"
if ($failedCount -gt 0) {
Write-Error "Some MCPB files failed to upload"
exit 1
}
Write-Host "`n✓ All MCPB files uploaded successfully"
displayName: "Upload MCPB to GitHub Release"
env:
GH_TOKEN: $(azuresdk-github-pat)
+11
View File
@@ -9,6 +9,8 @@ parameters:
type: boolean
- name: PackagePyPI
type: boolean
- name: PackageMCPB
type: boolean
- name: IsTestPipeline
type: boolean
@@ -75,6 +77,12 @@ jobs:
ServerName: ${{ parameters.ServerName }}
DependsOn: TagRepository
- ${{ if parameters.PackageMCPB }}:
- template: /eng/pipelines/templates/jobs/mcpb/release-mcpb.yml
parameters:
ServerName: ${{ parameters.ServerName }}
DependsOn: TagRepository
- ${{ if parameters.PackageDocker }}:
- template: /eng/pipelines/templates/jobs/docker/release-docker.yml
parameters:
@@ -86,6 +94,7 @@ jobs:
- template: /eng/pipelines/templates/jobs/update-mcp-repository.yml
parameters:
ServerName: ${{ parameters.ServerName }}
PackageMCPB: ${{ parameters.PackageMCPB }}
DependsOn:
- PublishNugetPackage
- PublishNpm
@@ -93,6 +102,8 @@ jobs:
- PublishACR
- ${{ if parameters.PackagePyPI }}:
- PublishPyPI
- ${{ if parameters.PackageMCPB }}:
- PublishMCPB
- job: UpdatePackageVersion
displayName: "Update Package Versions"
@@ -7,6 +7,8 @@ parameters:
type: boolean
- name: PackagePyPI
type: boolean
- name: PackageMCPB
type: boolean
jobs:
- job: Sign
@@ -126,6 +128,11 @@ jobs:
parameters:
DependsOn: Sign
- ${{ if parameters.PackageMCPB }}:
- template: /eng/pipelines/templates/jobs/mcpb/pack-and-sign-mcpb.yml
parameters:
DependsOn: Sign
- ${{ if parameters.PackageDocker }}:
- template: /eng/pipelines/templates/jobs/docker/stage-docker.yml
parameters:
@@ -6,6 +6,8 @@ parameters:
default: 'https://api.nuget.org/v3/index.json'
- name: DependsOn
type: object
- name: PackageMCPB
type: boolean
jobs:
- job: BuildAndUpdateMcpRepository
@@ -25,6 +27,22 @@ jobs:
displayName: Download build_info
artifact: build_info
- ${{ if parameters.PackageMCPB }}:
- download: current
displayName: Download signed MCPB packages
artifact: packages_mcpb_signed
- task: Powershell@2
displayName: 'Update server.json with MCPB SHA256 hashes'
inputs:
pwsh: true
filePath: $(Build.SourcesDirectory)/eng/scripts/Update-ServerJsonMcpbHashes.ps1
arguments: >
-ServerName '${{ parameters.ServerName }}'
-McpbPath '$(Pipeline.Workspace)/packages_mcpb_signed/${{ parameters.ServerName }}'
-ServerJsonPath '$(Pipeline.Workspace)/build_info/${{ parameters.ServerName }}/server.json'
-BuildInfoPath '$(Pipeline.Workspace)/build_info/build_info.json'
- task: Powershell@2
displayName: 'Verify Nuget package was published'
inputs:
+212
View File
@@ -0,0 +1,212 @@
#!/bin/env pwsh
#Requires -Version 7
<#
.SYNOPSIS
Applies detached PKCS#7 signatures to MCPB files.
.DESCRIPTION
This script takes MCPB files and their corresponding .signature.p7s files
(produced by ESRP's Pkcs7DetachedSign operation) and combines them into
signedFile MCPB files using the MCPB signature format.
The MCPB signature format (per https://github.com/modelcontextprotocol/mcpb/blob/main/CLI.md):
[Original MCPB ZIP content]
MCPB_SIG_V1
[4-byte little-endian length prefix]
[DER-encoded PKCS#7 signature]
MCPB_SIG_END
.PARAMETER ArtifactsPath
Path to the directory containing .mcpb files and their .signature.p7s files.
.PARAMETER OutputPath
Path to the output directory for signedFile MCPB files.
.EXAMPLE
./Apply-McpbSignatures.ps1 -ArtifactsPath "./to_sign" -OutputPath "./signedFile"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string] $ArtifactsPath,
[Parameter(Mandatory = $false)]
[string] $OutputPath
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/../common/scripts/common.ps1"
# MCPB signature markers (ASCII)
$SIG_V1_MARKER = "MCPB_SIG_V1"
$SIG_END_MARKER = "MCPB_SIG_END"
# Maximum signature block size, must match the value in Stage-McpbForSigning.ps1.
# The EOCD comment length is pre-set to this value before ESRP signing, so the
# signature block must be padded to exactly this size for the ZIP to be valid.
$MAX_SIG_BLOCK_SIZE = 16384
<#
.SYNOPSIS
Converts a PKCS#7 detached signature to MCPB embedded signature format.
#>
function Convert-P7sToMcpbSignature {
param(
[Parameter(Mandatory)]
[string] $P7sFile,
[Parameter(Mandatory)]
[string] $McpbFile,
[Parameter(Mandatory)]
[string] $OutputFile
)
# Validate inputs
if (-not (Test-Path $P7sFile)) {
throw "Signature file not found: $P7sFile"
}
if (-not (Test-Path $McpbFile)) {
throw "MCPB file not found: $McpbFile"
}
# Read signature bytes
$signatureBytes = [System.IO.File]::ReadAllBytes($P7sFile)
# Create length prefix (4-byte little-endian)
$lengthBytes = [BitConverter]::GetBytes([uint32]$signatureBytes.Length)
# Create markers as byte arrays
$sigV1MarkerBytes = [System.Text.Encoding]::ASCII.GetBytes($SIG_V1_MARKER)
$sigEndMarkerBytes = [System.Text.Encoding]::ASCII.GetBytes($SIG_END_MARKER)
# Read original MCPB content
$mcpbContent = [System.IO.File]::ReadAllBytes($McpbFile)
# Check if the MCPB is already signedFile by looking for the MCPB_SIG_END marker at the
# end of the file. This is always the last bytes of a signedFile MCPB, so we only need
# to read a small fixed-size tail rather than scanning the entire binary.
if ($mcpbContent.Length -ge $sigEndMarkerBytes.Length) {
$tailStart = $mcpbContent.Length - $sigEndMarkerBytes.Length
$tailString = [System.Text.Encoding]::ASCII.GetString($mcpbContent, $tailStart, $sigEndMarkerBytes.Length)
if ($tailString -eq $SIG_END_MARKER) {
throw "MCPB file appears to already be signedFile. Use 'mcpb unsign' to remove existing signature first."
}
}
# Calculate the unpadded signature block size:
# MCPB_SIG_V1 (11) + length (4) + signature (N) + MCPB_SIG_END (12) = N + 27
$unpaddedBlockSize = $sigV1MarkerBytes.Length + $lengthBytes.Length + $signatureBytes.Length + $sigEndMarkerBytes.Length
if ($unpaddedBlockSize -gt $MAX_SIG_BLOCK_SIZE) {
throw "Signature block ($unpaddedBlockSize bytes) exceeds MAX_SIG_BLOCK_SIZE ($MAX_SIG_BLOCK_SIZE bytes). Increase MAX_SIG_BLOCK_SIZE in both Stage-McpbForSigning.ps1 and Apply-McpbSignatures.ps1."
}
# Pad with zeros so the total signature block equals MAX_SIG_BLOCK_SIZE.
# This makes the ZIP valid because EOCD comment_length == MAX_SIG_BLOCK_SIZE.
# The padding sits between the DER signature and MCPB_SIG_END. The mcpb
# extraction logic reads exactly sigLength bytes via the 4-byte length prefix
# and ignores the padding.
$paddingSize = $MAX_SIG_BLOCK_SIZE - $unpaddedBlockSize
$paddingBytes = [byte[]]::new($paddingSize)
# Combine: MCPB + MCPB_SIG_V1 + length + signature + padding + MCPB_SIG_END
$signedContent = [System.Collections.Generic.List[byte]]::new()
$signedContent.AddRange($mcpbContent)
$signedContent.AddRange($sigV1MarkerBytes)
$signedContent.AddRange($lengthBytes)
$signedContent.AddRange($signatureBytes)
$signedContent.AddRange($paddingBytes)
$signedContent.AddRange($sigEndMarkerBytes)
[System.IO.File]::WriteAllBytes($OutputFile, $signedContent.ToArray())
}
# Validate required parameters when running as script
if (-not $ArtifactsPath) {
LogError "ArtifactsPath is required"
exit 1
}
if (-not $OutputPath) {
LogError "OutputPath is required"
exit 1
}
# Main script logic
if (!(Test-Path $ArtifactsPath)) {
LogError "Artifacts directory not found: $ArtifactsPath"
exit 1
}
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
LogInfo "Applying signatures to MCPB files..."
# Find all .mcpb files
$mcpbFiles = Get-ChildItem -Path $ArtifactsPath -Filter "*.mcpb" -Recurse
if ($mcpbFiles.Count -eq 0) {
LogError "No .mcpb files found in $ArtifactsPath"
exit 1
}
$signedFiles = [System.Collections.Generic.List[string]]::new()
$failedFiles = [System.Collections.Generic.List[string]]::new()
foreach ($mcpb in $mcpbFiles) {
$sigFile = Join-Path $mcpb.Directory.FullName ($mcpb.BaseName + ".signature.p7s")
if (-not (Test-Path $sigFile)) {
LogWarning "No signature file found for $($mcpb.Name)"
$failedFiles.Add($mcpb.Name)
continue
}
# Preserve directory structure in output
$pathativePath = $mcpb.Directory.FullName.Substring((Resolve-Path $ArtifactsPath).Path.Length).TrimStart('\', '/')
$targetDir = Join-Path $OutputPath $pathativePath
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
$outputFile = Join-Path $targetDir $mcpb.Name
LogInfo " Signing: $($mcpb.Name)"
try {
Convert-P7sToMcpbSignature -P7sFile $sigFile -McpbFile $mcpb.FullName -OutputFile $outputFile
}
catch {
LogError "Failed to sign $($mcpb.Name): $_"
$failedFiles.Add($mcpb.Name)
continue
}
if (-not (Test-Path $outputFile)) {
LogError "Failed to create signedFile MCPB: $outputFile"
$failedFiles.Add($mcpb.Name)
continue
}
$signedFiles.Add($outputFile)
}
LogInfo "`nSigned MCPB files:"
foreach ($signedFile in $signedFiles) {
$fileInfo = Get-Item $signedFile
$relativePath = $fileInfo.FullName.Substring((Resolve-Path $OutputPath).Path.Length).TrimStart('\', '/')
LogInfo " $relativePath ($($fileInfo.Length) bytes)"
}
LogInfo "`nSigning complete: $($signedFiles.Count) succeeded, $($failedFiles.Count) failed"
if ($failedFiles.Count -gt 0) {
LogError "Some MCPB files failed to sign"
exit 1
}
+3 -1
View File
@@ -60,7 +60,9 @@ $propertyList = @(
'IsAotCompatible',
'McpRepositoryName'
'McpRepositoryName',
'McpbPlatforms'
)
$projectFile = $projectFiles | Select-Object -First 1
+1
View File
@@ -531,6 +531,7 @@ function Get-ServerDetails {
pypiPackageKeywords = @($props.PypiPackageKeywords -split '[;,] *' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
platforms = $platforms
mcpRepositoryName = $props.McpRepositoryName
mcpbPlatforms = @($props.McpbPlatforms -split '[;,] *' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' })
serverJsonPath = $props.ServerJsonPath | Get-RepoRelativePath -NormalizeSeparators
}
}
+19 -1
View File
@@ -64,7 +64,9 @@ ReplacePropertyValue -HashTable $jsonHashTable -PropertyName 'name' -NewValue $s
ReplacePropertyValue -HashTable $jsonHashTable -PropertyName 'version' -NewValue $server.version
foreach ($package in $jsonHashTable.packages) {
ReplacePropertyValue -HashTable $package -PropertyName 'version' -NewValue $server.version
if ($package.ContainsKey('version')) {
ReplacePropertyValue -HashTable $package -PropertyName 'version' -NewValue $server.version
}
switch ($package.registryType) {
'nuget' {
@@ -85,6 +87,22 @@ foreach ($package in $jsonHashTable.packages) {
}
}
# Generate MCPB package entries from the McpbPlatforms csproj property
if ($server.mcpbPlatforms -and $server.mcpbPlatforms.Count -gt 0) {
foreach ($mcpbPlatform in $server.mcpbPlatforms) {
$mcpbFilename = "$($server.name)-$mcpbPlatform.mcpb"
$mcpbUrl = "https://github.com/microsoft/mcp/releases/download/$($server.releaseTag)/$mcpbFilename"
$jsonHashTable.packages += @{
registryType = 'mcpb'
identifier = $mcpbUrl
# fileSha256 will be set by Update-ServerJsonMcpbHashes.ps1 after signing
fileSha256 = ''
transport = @{ type = 'stdio' }
}
}
}
$updatedJson = $jsonHashTable | ConvertTo-Json -Depth 10
if (!$OutputPath) {
+253
View File
@@ -0,0 +1,253 @@
#!/bin/env pwsh
#Requires -Version 7
<#
.SYNOPSIS
Creates MCPB (MCP Bundle) packages from trimmed server binaries.
.DESCRIPTION
This script packages MCP servers into the MCPB format using the official MCPB CLI tool.
It reads server and platform information from build_info.json and creates unsigned .mcpb
files for each platform. The packages can then be signed using ESRP in a separate step.
.PARAMETER ArtifactsPath
Path to the build artifacts directory containing the trimmed server binaries.
Defaults to ".work/build" in the repo root.
.PARAMETER BuildInfoPath
Path to the build_info.json file containing server and platform details.
Defaults to ".work/build_info.json" in the repo root.
.PARAMETER OutputPath
Output directory for the .mcpb files.
Defaults to ".work/packages_mcpb" in the repo root.
.PARAMETER KeepStagingDirectory
If specified, keeps the staging directory after packaging for debugging purposes.
The staging directory is located at ".work/temp_mcpb" in the repo root.
.EXAMPLE
./Pack-Mcpb.ps1
.EXAMPLE
./Pack-Mcpb.ps1 -ArtifactsPath ".work/build" -BuildInfoPath ".work/build_info.json" -OutputPath ".work/packages_mcpb"
#>
[CmdletBinding()]
param(
[string] $ArtifactsPath,
[string] $BuildInfoPath,
[string] $OutputPath,
[switch] $KeepStagingDirectory
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/../common/scripts/common.ps1"
$RepoRoot = $RepoRoot.Path.Replace('\', '/')
$ignoreMissingArtifacts = $env:TF_BUILD -ne 'true'
$exitCode = 0
if (!$ArtifactsPath) {
$ArtifactsPath = "$RepoRoot/.work/build"
}
if (!$BuildInfoPath) {
$BuildInfoPath = "$RepoRoot/.work/build_info.json"
}
if (!$OutputPath) {
$OutputPath = "$RepoRoot/.work/packages_mcpb"
}
if (!(Test-Path $ArtifactsPath)) {
LogError "Artifacts path $ArtifactsPath does not exist."
$exitCode = 1
}
if (!(Test-Path $BuildInfoPath)) {
LogError "Build info file $BuildInfoPath does not exist. Run eng/scripts/New-BuildInfo.ps1 to create it."
$exitCode = 1
}
if ($exitCode -ne 0) {
exit $exitCode
}
# Restore MCPB CLI from local tool manifest (.config/dotnet-tools.json)
LogInfo "Restoring MCPB CLI..."
Invoke-LoggedCommand "dotnet tool restore" -GroupOutput
$buildInfo = Get-Content $BuildInfoPath -Raw | ConvertFrom-Json
if (Test-Path $OutputPath) {
LogInfo "Cleaning existing output path $OutputPath"
Remove-Item -Path $OutputPath -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path $OutputPath | Out-Null
$tempPath = "$RepoRoot/.work/temp_mcpb"
foreach ($server in $buildInfo.servers) {
$mcpbDirectory = "$RepoRoot/servers/$($server.name)/mcpb"
$manifestPath = "$mcpbDirectory/manifest.json"
if (!(Test-Path $manifestPath)) {
LogWarning "MCPB manifest not found at $manifestPath. Skipping server $($server.name)."
continue
}
LogInfo "`n========================================"
LogInfo "Packing MCPB for server: $($server.name)"
LogInfo "Version: $($server.version)"
LogInfo "========================================`n"
# Filter platforms: only trimmed, non-native, non-special-purpose platforms
$filteredPlatforms = $server.platforms | Where-Object {
$_.trimmed -eq $true -and
-not $_.native -and
-not $_.specialPurpose
}
if ($filteredPlatforms.Count -eq 0) {
LogInfo "No suitable platforms found for MCPB packaging for server $($server.name)"
continue
}
foreach ($platform in $filteredPlatforms) {
$platformDirectory = "$ArtifactsPath/$($platform.artifactPath)"
$platformName = $platform.name
if (!(Test-Path $platformDirectory)) {
$message = "Platform directory $platformDirectory does not exist."
if ($ignoreMissingArtifacts) {
LogWarning $message
continue
} else {
LogError $message
$exitCode = 1
continue
}
}
LogInfo @"
------------------------------------------------------------------------------------
Processing MCPB packaging:
Server: $($server.name)
Platform: $platformName
Version: $($server.version)
CLI Name: $($server.cliName)
Binaries Path: $platformDirectory
------------------------------------------------------------------------------------
"@
# Create clean staging directory for this platform
$stagingDir = "$tempPath/$($server.name)/$platformName"
Remove-Item -Path $stagingDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $stagingDir | Out-Null
New-Item -ItemType Directory -Force -Path "$stagingDir/server" | Out-Null
# Copy trimmed binaries to server/ subdirectory
LogInfo "Copying trimmed binaries from $platformDirectory..."
Copy-Item -Path "$platformDirectory/*" -Destination "$stagingDir/server" -Recurse
# Copy and update manifest.json for this platform
# The source manifest uses platform-agnostic paths (no extension), so we need to add
# the correct extension for this platform's executable
LogInfo "Copying manifest from $manifestPath..."
$manifest = Get-Content $manifestPath -Raw | ConvertFrom-Json
$executableName = $server.cliName + $platform.extension
# Update version from build_info.json (source of truth for all packaging)
$manifest.version = $server.version
# entry_point always uses forward slashes (required by MCPB CLI validation)
$manifest.server.entry_point = "server/$executableName"
# command uses backslashes for Windows paths, forward slashes for all other platforms
$pathSep = if ($platform.operatingSystem -eq 'windows') { '\' } else { '/' }
$manifest.server.mcp_config.command = "`${__dirname}${pathSep}server${pathSep}$executableName"
# Remove platform_overrides since this bundle is platform-specific
if ($manifest.server.mcp_config.PSObject.Properties.Name -contains 'platform_overrides') {
$manifest.server.mcp_config.PSObject.Properties.Remove('platform_overrides')
}
$manifest | ConvertTo-Json -Depth 100 | Set-Content "$stagingDir/manifest.json" -NoNewline
# Copy and rename icon to servericon.png (required name for MCPB bundles)
$packageIconPath = "$RepoRoot/$($server.packageIcon)"
if (Test-Path $packageIconPath) {
LogInfo "Copying icon from $packageIconPath..."
Copy-Item -Path $packageIconPath -Destination "$stagingDir/servericon.png" -Force
} else {
LogWarning "No icon found for $($server.name). MCPB may not validate."
}
# Copy LICENSE and NOTICE.txt
Copy-Item -Path "$RepoRoot/LICENSE" -Destination $stagingDir -Force
Copy-Item -Path "$RepoRoot/NOTICE.txt" -Destination $stagingDir -Force
# Validate manifest
LogInfo "Validating manifest..."
$validateResult = dotnet mcpb validate $stagingDir 2>&1
if ($LASTEXITCODE -ne 0) {
LogError "Manifest validation failed for $($server.name) on $platformName"
LogError $validateResult
$exitCode = 1
continue
}
LogInfo "Manifest validation passed"
# Create output directory for this server/platform
$serverOutputPath = "$OutputPath/$($server.artifactPath)"
New-Item -ItemType Directory -Force -Path $serverOutputPath | Out-Null
# Pack the MCPB
# Use --update to auto-update manifest with discovered tools from the server
$mcpbFileName = "$($server.name)-$($platform.dotnetOs)-$($platform.architecture).mcpb"
$mcpbFilePath = "$serverOutputPath/$mcpbFileName"
LogInfo "Packing MCPB to $mcpbFilePath..."
& dotnet mcpb pack $stagingDir $mcpbFilePath --update
if ($LASTEXITCODE -ne 0) {
LogError "MCPB packing failed for $($server.name) on $platformName"
$exitCode = 1
continue
}
# Get file size for reporting
$fileSize = (Get-Item $mcpbFilePath).Length
$fileSizeMB = [math]::Round($fileSize / 1MB, 2)
LogInfo "Created: $mcpbFilePath ($fileSizeMB MB)"
# Show package info
LogInfo "`nPackage info:"
dotnet mcpb info $mcpbFilePath
}
}
# Cleanup temp directory (unless -KeepStagingDirectory is specified)
if ($KeepStagingDirectory) {
LogWarning "`nStaging directory preserved at: $tempPath"
} else {
Remove-Item -Path $tempPath -Recurse -Force -ErrorAction SilentlyContinue
}
if ($exitCode -eq 0) {
LogInfo "`n========================================"
LogInfo "MCPB packaging completed successfully!"
LogInfo "Output: $OutputPath"
LogInfo "========================================`n"
} else {
LogError "`n========================================"
LogError "MCPB packaging completed with errors."
LogError "========================================`n"
}
exit $exitCode
+141
View File
@@ -0,0 +1,141 @@
#!/bin/env pwsh
#Requires -Version 7
<#
.SYNOPSIS
Stages MCPB files for ESRP detached signing.
.DESCRIPTION
This script prepares MCPB files for ESRP's Pkcs7DetachedSign operation.
ESRP replaces the input file content with the signature, so we:
1. Copy each .mcpb file to the staging directory
2. Create a .signature.p7s copy for ESRP to process
After ESRP signing, the .signature.p7s files will contain the detached signatures.
.PARAMETER ArtifactsPath
Path to the directory containing unsigned MCPB files.
.PARAMETER OutputPath
Path to the staging directory for ESRP signing.
.EXAMPLE
./Stage-McpbForSigning.ps1 -ArtifactsPath "./mcpb" -OutputPath "./to_sign"
#>
param(
[Parameter(Mandatory = $true)]
[string] $ArtifactsPath,
[Parameter(Mandatory = $true)]
[string] $OutputPath
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/../common/scripts/common.ps1"
# Maximum signature block size in bytes. The ZIP EOCD comment length is set to this
# value before signing so that the final signed file remains a valid ZIP archive.
# The signature block (MCPB_SIG_V1 + length + signature + padding + MCPB_SIG_END)
# is padded with zeros to exactly this size.
# Current ESRP signatures are ~4KB; 16384 provides ample headroom.
$MAX_SIG_BLOCK_SIZE = 16384
<#
.SYNOPSIS
Updates the ZIP End of Central Directory (EOCD) comment length field.
.DESCRIPTION
Sets the EOCD comment length to a fixed value so that after the MCPB signature
block is appended (and padded to this exact size), the file remains a valid ZIP.
This is necessary because some ZIP parsers (e.g., Claude Desktop) strictly
validate that file_size == EOCD_offset + 22 + comment_length.
#>
function Set-ZipEocdCommentLength {
param(
[Parameter(Mandatory)]
[string] $FilePath,
[Parameter(Mandatory)]
[uint16] $CommentLength
)
$bytes = [System.IO.File]::ReadAllBytes($FilePath)
$len = $bytes.Length
# EOCD signature: 0x06054b50 (PK\x05\x06)
$eocdSig = @([byte]0x50, [byte]0x4B, [byte]0x05, [byte]0x06)
$eocdOffset = -1
# Search backwards from end of file (EOCD is at most 65557 bytes from end)
$searchStart = [Math]::Max(0, $len - 65557)
for ($i = $len - 22; $i -ge $searchStart; $i--) {
if ($bytes[$i] -eq $eocdSig[0] -and $bytes[$i+1] -eq $eocdSig[1] -and
$bytes[$i+2] -eq $eocdSig[2] -and $bytes[$i+3] -eq $eocdSig[3]) {
$eocdOffset = $i
break
}
}
if ($eocdOffset -lt 0) {
throw "ZIP EOCD signature not found in $FilePath"
}
# EOCD comment length is at offset 20-21 (2 bytes, little-endian)
$commentLenBytes = [BitConverter]::GetBytes($CommentLength)
$bytes[$eocdOffset + 20] = $commentLenBytes[0]
$bytes[$eocdOffset + 21] = $commentLenBytes[1]
[System.IO.File]::WriteAllBytes($FilePath, $bytes)
}
if (!(Test-Path $ArtifactsPath)) {
LogError "MCPB directory not found: $ArtifactsPath"
exit 1
}
New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
LogInfo "Staging MCPB files for signing..."
# Find all .mcpb files recursively (they're organized by server name)
$mcpbFiles = Get-ChildItem -Path $ArtifactsPath -Filter "*.mcpb" -Recurse
if ($mcpbFiles.Count -eq 0) {
LogError "No .mcpb files found in $ArtifactsPath"
exit 1
}
foreach ($mcpb in $mcpbFiles) {
# Preserve directory structure
$relativePath = $mcpb.Directory.FullName.Substring((Resolve-Path $ArtifactsPath).Path.Length).TrimStart('\', '/')
$targetDir = Join-Path $OutputPath $relativePath
if (-not (Test-Path $targetDir)) {
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
}
# Copy original .mcpb and update EOCD comment length.
# The comment length is set to MAX_SIG_BLOCK_SIZE so that after the signature
# block is appended (padded to this size), the file is a valid ZIP. ESRP signs
# this modified content, so mcpb verify still works because the "original content"
# extracted during verification matches what was signed.
$mcpbDest = Join-Path $targetDir $mcpb.Name
Copy-Item $mcpb.FullName $mcpbDest -Force
Set-ZipEocdCommentLength -FilePath $mcpbDest -CommentLength $MAX_SIG_BLOCK_SIZE
LogInfo " Updated EOCD comment length to $MAX_SIG_BLOCK_SIZE for $($mcpb.Name)"
# Create .signature.p7s copy for ESRP to sign (same modified content)
$sigName = $mcpb.BaseName + ".signature.p7s"
$sigDest = Join-Path $targetDir $sigName
Copy-Item $mcpbDest $sigDest -Force
LogInfo " Staged: $($mcpb.Name) -> $sigName"
}
LogInfo "`nFiles staged for signing:"
Get-ChildItem -Path $OutputPath -Recurse -File | ForEach-Object {
$rel = $_.FullName.Substring((Resolve-Path $OutputPath).Path.Length).TrimStart('\', '/')
LogInfo " $rel ($($_.Length) bytes)"
}
LogInfo "`nStaged $($mcpbFiles.Count) MCPB file(s) for signing"
+134
View File
@@ -0,0 +1,134 @@
#!/bin/env pwsh
#Requires -Version 7
<#
.SYNOPSIS
Updates the server.json file with SHA256 hashes for signed MCPB packages.
.DESCRIPTION
This script computes SHA256 hashes for signed MCPB files and updates the
corresponding fileSha256 fields in server.json. This is required for
publishing to the MCP Registry, which validates file integrity.
.PARAMETER ServerName
Name of the server (e.g., Azure.Mcp.Server).
.PARAMETER McpbPath
Path to the directory containing signed MCPB files.
.PARAMETER ServerJsonPath
Path to the server.json file to update.
.PARAMETER OutputPath
Optional path for the updated server.json. Defaults to updating in place.
.PARAMETER BuildInfoPath
Path to the build_info.json file containing server and platform details.
.EXAMPLE
./Update-ServerJsonMcpbHashes.ps1 -ServerName "Azure.Mcp.Server" `
-McpbPath "./packages_mcpb_signed/Azure.Mcp.Server" `
-ServerJsonPath "./build_info/Azure.Mcp.Server/server.json" `
-BuildInfoPath "./build_info/build_info.json"
#>
param(
[Parameter(Mandatory = $true)]
[string] $ServerName,
[Parameter(Mandatory = $true)]
[string] $McpbPath,
[Parameter(Mandatory = $true)]
[string] $ServerJsonPath,
[Parameter(Mandatory = $true)]
[string] $BuildInfoPath,
[string] $OutputPath
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/../common/scripts/common.ps1"
if (!(Test-Path $ServerJsonPath)) {
LogError "Server JSON file not found: $ServerJsonPath"
exit 1
}
if (!(Test-Path $McpbPath)) {
LogError "MCPB directory not found: $McpbPath"
exit 1
}
if (!(Test-Path $BuildInfoPath)) {
LogError "Build info file not found: $BuildInfoPath"
exit 1
}
# Load platform data from build info
$buildInfo = Get-Content $BuildInfoPath -Raw | ConvertFrom-Json
$server = $buildInfo.servers | Where-Object { $_.name -ieq $ServerName }
if (!$server) {
LogError "Server '$ServerName' not found in build info file $BuildInfoPath."
exit 1
}
# Find all MCPB files and compute their SHA256 hashes
$mcpbFiles = Get-ChildItem -Path $McpbPath -Filter "*.mcpb" -Recurse
$hashes = @{}
foreach ($mcpb in $mcpbFiles) {
# Extract platform from filename (e.g., "Azure.Mcp.Server-win-x64.mcpb" -> "win-x64")
if ($mcpb.BaseName -match "^$([regex]::Escape($ServerName))-(.+)$") {
$platform = $Matches[1]
# Compute SHA256 hash
$hash = (Get-FileHash -Path $mcpb.FullName -Algorithm SHA256).Hash.ToLower()
$hashes[$platform] = $hash
LogInfo " $($mcpb.Name): $hash"
} else {
LogWarning "Could not extract platform from MCPB filename: $($mcpb.Name)"
}
}
if ($hashes.Count -eq 0) {
LogError "No MCPB files found in $McpbPath"
exit 1
}
# Read and update server.json
$serverJson = Get-Content -Path $ServerJsonPath -Raw | ConvertFrom-Json -AsHashtable
$updatedCount = 0
foreach ($package in $serverJson.packages) {
if ($package.registryType -eq 'mcpb') {
# Find the platform from the identifier URL (e.g., "-win-x64.mcpb")
foreach ($mcpbPlatform in $server.mcpbPlatforms) {
if ($package.identifier -like "*-$mcpbPlatform.mcpb*") {
if ($hashes.ContainsKey($mcpbPlatform)) {
$package.fileSha256 = $hashes[$mcpbPlatform]
$updatedCount++
LogInfo "Updated SHA256 for $mcpbPlatform package"
} else {
LogWarning "No hash found for platform $mcpbPlatform"
}
break
}
}
}
}
if ($updatedCount -eq 0) {
LogWarning "No MCPB package hashes were updated in server.json"
}
# Write updated server.json
$outputFile = if ($OutputPath) { $OutputPath } else { $ServerJsonPath }
$updatedJson = $serverJson | ConvertTo-Json -Depth 10
New-Item -ItemType File -Force -Path $outputFile | Out-Null
Set-Content -Path $outputFile -Value $updatedJson -Encoding UTF8 -NoNewLine
LogInfo "Updated server.json written to $outputFile ($updatedCount MCPB hashes updated)"
+108
View File
@@ -0,0 +1,108 @@
#!/bin/env pwsh
#Requires -Version 7
<#
.SYNOPSIS
Verifies signatures on MCPB files.
.DESCRIPTION
This script verifies the signatures on all MCPB files in a directory
using the mcpb CLI tool. It provides detailed output about each file
and summarizes the verification results.
.PARAMETER ArtifactsPath
Path to the directory containing signed MCPB files.
.PARAMETER FailOnError
If set, the script will exit with an error code if any verification fails,
including trust chain warnings. Without this flag, the script will still fail
on genuinely invalid signatures but will tolerate trust chain issues (expected
for Microsoft-signed packages when the certificate chain is not in the trust store).
.EXAMPLE
./Verify-McpbSignatures.ps1 -ArtifactsPath "./signed"
.EXAMPLE
./Verify-McpbSignatures.ps1 -ArtifactsPath "./signed" -FailOnError
#>
param(
[Parameter(Mandatory = $true)]
[string] $ArtifactsPath,
[switch] $FailOnError
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/../common/scripts/common.ps1"
if (!(Test-Path $ArtifactsPath)) {
LogError "MCPB directory not found: $ArtifactsPath"
exit 1
}
# Restore MCPB CLI from local tool manifest (.config/dotnet-tools.json)
LogInfo "Restoring MCPB CLI..."
Invoke-LoggedCommand "dotnet tool restore" -GroupOutput
LogInfo "Verifying signed MCPB files..."
$mcpbFiles = Get-ChildItem -Path $ArtifactsPath -Filter "*.mcpb" -Recurse
if ($mcpbFiles.Count -eq 0) {
LogError "No .mcpb files found in $ArtifactsPath"
exit 1
}
$passedCount = 0
$warningCount = 0
$failedCount = 0
foreach ($mcpb in $mcpbFiles) {
LogInfo "`n=== Verifying: $($mcpb.Name) ==="
# Show bundle info
& dotnet mcpb info $mcpb.FullName
# Verify signature and capture output for classification
$verifyOutput = & dotnet mcpb verify $mcpb.FullName 2>&1 | Out-String
$verifyExitCode = $LASTEXITCODE
LogInfo $verifyOutput
if ($verifyExitCode -eq 0) {
LogInfo "✓ $($mcpb.Name) - Signature verified"
$passedCount++
} else {
# Distinguish untrusted certificate chain (expected in CI for Microsoft-signed
# packages) from genuinely invalid or corrupt signatures.
$isTrustChainIssue = $verifyOutput -match '(?i)(untrusted|chain|trust|certificate.*not found|root.*not.*trusted|certificate.*expired)'
if ($isTrustChainIssue) {
LogWarning "✗ $($mcpb.Name) - Certificate chain not trusted (exit code $verifyExitCode). This is expected for Microsoft-signed packages in CI."
$warningCount++
} else {
LogError "✗ $($mcpb.Name) - Signature verification failed (exit code $verifyExitCode)"
$failedCount++
}
}
}
LogInfo "`n=== Verification Summary ==="
LogInfo " Passed: $passedCount"
LogInfo " Warnings (trust chain): $warningCount"
LogInfo " Failed (invalid signature): $failedCount"
LogInfo " Total: $($mcpbFiles.Count)"
# Always fail on genuinely invalid signatures regardless of -FailOnError
if ($failedCount -gt 0) {
LogError "Some MCPB files have invalid signatures"
exit 1
}
# Trust chain warnings are only fatal when -FailOnError is set
if ($FailOnError -and $warningCount -gt 0) {
LogError "Some MCPB files had trust chain warnings and -FailOnError is set"
exit 1
}
LogInfo "`nVerification complete"
+4
View File
@@ -12,6 +12,9 @@ parameters:
- name: PackagePyPI
type: boolean
default: true
- name: PackageMCPB
type: boolean
default: true
- name: IncludeNative
type: boolean
default: true
@@ -32,5 +35,6 @@ extends:
PackageDocker: ${{ parameters.PackageDocker }}
PackageVSIX: ${{ parameters.PackageVSIX }}
PackagePyPI: ${{ parameters.PackagePyPI }}
PackageMCPB: ${{ parameters.PackageMCPB }}
IncludeNative: ${{ parameters.IncludeNative }}
TestTimeoutInMinutes: ${{ parameters.TestTimeoutInMinutes }}
@@ -0,0 +1,4 @@
pr: 1681
changes:
- section: "Features Added"
description: "Added support for publishing server as an MCP Bundle (.mcpb)"
+109
View File
@@ -0,0 +1,109 @@
{
"manifest_version": "0.3",
"name": "Azure.Mcp.Server",
"display_name": "Azure MCP Server",
"version": "2.0.0-beta.17",
"description": "Provides Model Context Protocol (MCP) integration and tooling for Microsoft Azure.",
"long_description": "All Azure MCP tools in a single server. The Azure MCP Server implements the [MCP specification](https://modelcontextprotocol.io) to create a seamless connection between AI agents and Azure services. Azure MCP Server can be used alone or with the [GitHub Copilot for Azure extension](https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azure-github-copilot) in VS Code.",
"author": {
"name": "Microsoft",
"url": "https://www.microsoft.com"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/mcp.git"
},
"homepage": "https://learn.microsoft.com/azure/developer/azure-mcp-server/get-started",
"documentation": "https://learn.microsoft.com/azure/developer/azure-mcp-server",
"support": "https://github.com/microsoft/mcp/issues",
"icon": "./servericon.png",
"server": {
"type": "binary",
"entry_point": "server/azmcp",
"mcp_config": {
"platform_overrides": {
"win32": {
"command": "${__dirname}\\server\\azmcp.exe",
"args": []
}
},
"command": "${__dirname}/server/azmcp",
"args": [ "server", "start" ],
"env": {}
}
},
"tools": [],
"tools_generated": false,
"keywords": [
"azure",
"mcp",
"model context protocol",
"server",
"microsoft",
"ai",
"artificial intelligence",
"acr",
"azure container registry",
"aks",
"kubernetes",
"app configuration",
"applens",
"application insights",
"app service",
"terraform",
"bicep",
"cloud",
"architecture",
"communication services",
"confidential ledger",
"cosmos",
"database",
"datadog",
"deploy",
"event grid",
"event hubs",
"foundry",
"function app",
"grafana",
"resource group",
"key vault",
"kusto",
"data explorer",
"load testing",
"managed lustre",
"marketplace",
"monitor",
"mysql",
"postgres",
"postgresql",
"quota",
"redis",
"resource health",
"role assignments",
"rbac",
"search",
"service bus",
"signalr",
"speech",
"sql",
"storage",
"subscription",
"virtual desktop",
"workbooks",
"azqr",
"quick reference",
"cli"
],
"license": "MIT",
"privacy_policies": [
"https://www.microsoft.com/privacy/privacystatement"
],
"compatibility": {
"claude_desktop": ">=0.8.0",
"platforms": [
"darwin",
"win32",
"linux"
]
}
}
@@ -27,6 +27,9 @@
<!-- pypi settings -->
<PypiPackageName>msmcp-azure</PypiPackageName>
<PypiPackageKeywords>azure,mcp,model-context-protocol,ai,llm</PypiPackageKeywords>
<!-- mcpb settings -->
<McpbPlatforms>win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</McpbPlatforms>
</PropertyGroup>
<PropertyGroup>
+2 -1
View File
@@ -26,8 +26,9 @@ extends:
ServerName: Fabric.Mcp.Server
PublishTarget: ${{ parameters.PublishTarget }}
RunLiveTests: true
PackagePyPI: false
PackageDocker: ${{ parameters.PackageDocker }}
PackageVSIX: ${{ parameters.PackageVSIX }}
PackagePyPI: false
PackageMCPB: false
IncludeNative: ${{ parameters.IncludeNative }}
TestTimeoutInMinutes: ${{ parameters.TestTimeoutInMinutes }}
@@ -0,0 +1,60 @@
{
"manifest_version": "0.3",
"name": "Fabric.Mcp.Server",
"display_name": "Microsoft Fabric MCP Server",
"version": "0.0.0-beta.7",
"description": "Provides Model Context Protocol (MCP) integration and tooling for Microsoft Fabric.",
"long_description": "A local-first Model Context Protocol (MCP) server that provides AI agents with comprehensive access to Microsoft Fabric's public APIs, item definitions, and best practices. The Fabric MCP Server packages complete OpenAPI specifications into a single context layer for AI-assisted development—without connecting to live Fabric environments.",
"author": {
"name": "Microsoft",
"url": "https://www.microsoft.com"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/mcp.git"
},
"homepage": "https://github.com/microsoft/mcp/tree/main/servers/Fabric.Mcp.Server",
"documentation": "https://github.com/microsoft/mcp/tree/main/servers/Fabric.Mcp.Server#readme",
"support": "https://github.com/microsoft/mcp/issues",
"icon": "./servericon.png",
"server": {
"type": "binary",
"entry_point": "server/fabmcp",
"mcp_config": {
"platform_overrides": {
"win32": {
"command": "${__dirname}\\server\\fabmcp.exe",
"args": []
}
},
"command": "${__dirname}/server/fabmcp",
"args": [ "server", "start" ],
"env": {}
}
},
"tools": [],
"tools_generated": false,
"keywords": [
"fabric",
"mcp",
"model context protocol",
"server",
"microsoft",
"ai",
"artificial intelligence",
"openapi",
"onelake"
],
"license": "MIT",
"privacy_policies": [
"https://www.microsoft.com/privacy/privacystatement"
],
"compatibility": {
"claude_desktop": ">=0.8.0",
"platforms": [
"darwin",
"win32",
"linux"
]
}
}
+4
View File
@@ -12,6 +12,9 @@ parameters:
- name: PackagePyPI
type: boolean
default: false
- name: PackageMCPB
type: boolean
default: false
- name: IncludeNative
type: boolean
default: true
@@ -32,6 +35,7 @@ extends:
PackageDocker: ${{ parameters.PackageDocker }}
PackageVSIX: ${{ parameters.PackageVSIX }}
PackagePyPI: ${{ parameters.PackagePyPI }}
PackageMCPB: ${{ parameters.PackageMCPB }}
IncludeNative: ${{ parameters.IncludeNative }}
TestTimeoutInMinutes: ${{ parameters.TestTimeoutInMinutes }}
IsTestPipeline: true
@@ -0,0 +1,50 @@
{
"manifest_version": "0.3",
"name": "Template.Mcp.Server",
"display_name": "Template MCP Server",
"version": "0.0.12",
"description": "Basic Model Context Protocol implementation for Microsoft servers.",
"long_description": "A template Model Context Protocol (MCP) server that provides a starting point for building new servers. This server implements the [MCP specification](https://modelcontextprotocol.io) and can be used as a reference implementation for creating custom MCP servers.",
"author": {
"name": "Microsoft",
"url": "https://www.microsoft.com"
},
"repository": {
"type": "git",
"url": "https://github.com/microsoft/mcp.git"
},
"homepage": "https://github.com/microsoft/mcp/tree/main/servers/Template.Mcp.Server",
"documentation": "https://github.com/microsoft/mcp/tree/main/servers/Template.Mcp.Server#readme",
"support": "https://github.com/microsoft/mcp/issues",
"icon": "./servericon.png",
"server": {
"type": "binary",
"entry_point": "server/mcptmp",
"mcp_config": {
"platform_overrides": {
"win32": {
"command": "${__dirname}\\server\\mcptmp.exe",
"args": []
}
},
"command": "${__dirname}/server/mcptmp",
"args": [ "server", "start" ],
"env": {}
}
},
"tools": [],
"tools_generated": false,
"keywords": [],
"license": "MIT",
"privacy_policies": [
"https://www.microsoft.com/privacy/privacystatement"
],
"compatibility": {
"claude_desktop": ">=0.8.0",
"platforms": [
"darwin",
"win32",
"linux"
]
}
}
@@ -27,6 +27,9 @@
<!-- pypi settings -->
<PypiPackageName>msmcp-template</PypiPackageName>
<PypiPackageKeywords>template,mcp,model-context-protocol,ai,llm</PypiPackageKeywords>
<!-- mcpb settings -->
<McpbPlatforms>win-x64;win-arm64;linux-x64;linux-arm64;osx-x64;osx-arm64</McpbPlatforms>
</PropertyGroup>
<PropertyGroup>