Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/reference/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,35 @@ components:
description: A mapping of environment variables to be set when running the package.
items:
$ref: '#/components/schemas/KeyValueInput'
postInstallInstructions:
type: array
description: "Structured post-install steps that clients can display as a checklist after downloading the package. Intended for servers that require additional manual setup beyond installation (for example, registering a companion system service). Instructions are informational only - clients display text and never execute commands automatically."
items:
$ref: '#/components/schemas/PostInstallInstruction'

PostInstallInstruction:
type: object
required:
- description
properties:
description:
type: string
description: Human-readable instruction text describing the post-install step.
minLength: 1
example: "Install as a system service for the web UI"
command:
type: string
description: Optional shell command the user can run to complete the step. Displayed for the user to copy; clients must not execute it automatically.
example: "mind-map service install --addr 127.0.0.1:4242"
documentation:
type: string
format: uri
description: Optional link to documentation or a setup page describing the step in more detail.
example: "https://github.com/aniongithub/mind-map#service-management"
optional:
type: boolean
description: Whether the step is optional. When true, the MCP server functions without completing this step.
default: false

Input:
type: object
Expand Down
27 changes: 27 additions & 0 deletions docs/reference/server-json/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ Changes to the server.json schema and format.

This section tracks changes that are in development and not yet released. The draft schema is available at [`server.schema.json`](./draft/server.schema.json) in this repository.

### Added

#### Optional `postInstallInstructions` on Packages

Packages may now declare an optional `postInstallInstructions` array of structured post-install steps that clients can display as a checklist after downloading a package. This is intended for servers that require additional manual setup beyond installation — for example, a server that doubles as a system service and needs a companion daemon registered. Instructions are informational only: clients display the text and never execute commands automatically, so there is no supply chain risk.

Each instruction has a required `description` and optional `command`, `documentation` (a URI), and `optional` (boolean) fields.

**Example:**
```json
{
"packages": [{
"registryType": "mcpb",
"identifier": "https://github.com/aniongithub/mind-map/releases/download/v1.0.0/mind-map.mcpb",
"transport": { "type": "stdio" },
"postInstallInstructions": [
{
"description": "Install as a system service for the web UI",
"command": "mind-map service install --addr 127.0.0.1:4242",
"documentation": "https://github.com/aniongithub/mind-map#service-management",
"optional": true
}
]
}]
}
```

### Changed

#### Transport URL Pattern Now Accepts Template Variables
Expand Down
37 changes: 37 additions & 0 deletions docs/reference/server-json/draft/server.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,13 @@
},
"type": "array"
},
"postInstallInstructions": {
"description": "Structured post-install steps that clients can display as a checklist after downloading the package. Intended for servers that require additional manual setup beyond installation (for example, registering a companion system service). Instructions are informational only - clients display text and never execute commands automatically.",
"items": {
"$ref": "#/definitions/PostInstallInstruction"
},
"type": "array"
},
"registryBaseUrl": {
"description": "Base URL of the package registry",
"examples": [
Expand Down Expand Up @@ -344,6 +351,36 @@
],
"description": "A positional input is a value inserted verbatim into the command line."
},
"PostInstallInstruction": {
"properties": {
"command": {
"description": "Optional shell command the user can run to complete the step. Displayed for the user to copy; clients must not execute it automatically.",
"example": "mind-map service install --addr 127.0.0.1:4242",
"type": "string"
},
"description": {
"description": "Human-readable instruction text describing the post-install step.",
"example": "Install as a system service for the web UI",
"minLength": 1,
"type": "string"
},
"documentation": {
"description": "Optional link to documentation or a setup page describing the step in more detail.",
"example": "https://github.com/aniongithub/mind-map#service-management",
"format": "uri",
"type": "string"
},
"optional": {
"default": false,
"description": "Whether the step is optional. When true, the MCP server functions without completing this step.",
"type": "boolean"
}
},
"required": [
"description"
],
"type": "object"
},
"RemoteTransport": {
"allOf": [
{
Expand Down
156 changes: 156 additions & 0 deletions internal/validators/post_install_instructions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package validators_test

import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

"github.com/santhosh-tekuri/jsonschema/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// draftSchemaPath is the in-repo draft schema, the source of truth for unreleased
// schema changes such as postInstallInstructions.
const draftSchemaPath = "../../docs/reference/server-json/draft/server.schema.json"

// compileDraftSchema loads and compiles the in-repo draft server.json schema so
// tests can validate documents against the unreleased schema definition.
func compileDraftSchema(t *testing.T) *jsonschema.Schema {
t.Helper()

absPath, err := filepath.Abs(draftSchemaPath)
require.NoError(t, err, "resolve draft schema path")

schemaData, err := os.ReadFile(absPath)
require.NoError(t, err, "read draft schema file")

schemaID := "https://raw.githubusercontent.com/modelcontextprotocol/registry/main/docs/reference/server-json/draft/server.schema.json"

compiler := jsonschema.NewCompiler()
require.NoError(t, compiler.AddResource(schemaID, strings.NewReader(string(schemaData))), "add draft schema resource")

schema, err := compiler.Compile(schemaID)
require.NoError(t, err, "compile draft schema")
return schema
}

// validateAgainstDraft validates a raw server.json document against the draft schema
// and returns any validation error.
func validateAgainstDraft(t *testing.T, schema *jsonschema.Schema, doc string) error {
t.Helper()

var instance interface{}
require.NoError(t, json.Unmarshal([]byte(doc), &instance), "unmarshal server.json document")
return schema.Validate(instance)
}

const baseServerWithPackagePrefix = `{
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
"name": "io.github.aniongithub/mind-map",
"description": "A mind-map MCP server that doubles as a system service",
"version": "1.0.0",
"packages": [`

const baseServerSuffix = `]
}`

// TestDraftSchema_PostInstallInstructions_Accepted ensures a package carrying
// postInstallInstructions validates cleanly against the draft schema.
func TestDraftSchema_PostInstallInstructions_Accepted(t *testing.T) {
schema := compileDraftSchema(t)

doc := baseServerWithPackagePrefix + `{
"registryType": "mcpb",
"identifier": "https://github.com/aniongithub/mind-map/releases/download/v1.0.0/mind-map.mcpb",
"version": "1.0.0",
"fileSha256": "fe333e598595000ae021bd27117db32ec69af6987f507ba7a63c90638ff633ce",
"transport": { "type": "stdio" },
"postInstallInstructions": [
{
"description": "Install as a system service for the web UI",
"command": "mind-map service install --addr 127.0.0.1:4242",
"documentation": "https://github.com/aniongithub/mind-map#service-management",
"optional": true
}
]
}` + baseServerSuffix

err := validateAgainstDraft(t, schema, doc)
assert.NoError(t, err, "server.json with postInstallInstructions should be valid")
}

// TestDraftSchema_PostInstallInstructions_OptionalWhenAbsent ensures the field
// remains optional: a package without it must still validate.
func TestDraftSchema_PostInstallInstructions_OptionalWhenAbsent(t *testing.T) {
schema := compileDraftSchema(t)

doc := baseServerWithPackagePrefix + `{
"registryType": "npm",
"identifier": "@example/server",
"version": "1.0.0",
"transport": { "type": "stdio" }
}` + baseServerSuffix

err := validateAgainstDraft(t, schema, doc)
assert.NoError(t, err, "server.json without postInstallInstructions should remain valid")
}

// TestDraftSchema_PostInstallInstructions_MinimalDescriptionOnly ensures only the
// required description field is needed for a valid instruction.
func TestDraftSchema_PostInstallInstructions_MinimalDescriptionOnly(t *testing.T) {
schema := compileDraftSchema(t)

doc := baseServerWithPackagePrefix + `{
"registryType": "npm",
"identifier": "@example/server",
"version": "1.0.0",
"transport": { "type": "stdio" },
"postInstallInstructions": [
{ "description": "Restart your editor to load the server" }
]
}` + baseServerSuffix

err := validateAgainstDraft(t, schema, doc)
assert.NoError(t, err, "instruction with only a description should be valid")
}

// TestDraftSchema_PostInstallInstructions_RequiresDescription ensures an
// instruction missing the required description field is rejected.
func TestDraftSchema_PostInstallInstructions_RequiresDescription(t *testing.T) {
schema := compileDraftSchema(t)

doc := baseServerWithPackagePrefix + `{
"registryType": "npm",
"identifier": "@example/server",
"version": "1.0.0",
"transport": { "type": "stdio" },
"postInstallInstructions": [
{ "command": "do something" }
]
}` + baseServerSuffix

err := validateAgainstDraft(t, schema, doc)
assert.Error(t, err, "instruction without description should be rejected")
}

// TestDraftSchema_PostInstallInstructions_DocumentationMustBeURI ensures the
// documentation field is validated as a URI.
func TestDraftSchema_PostInstallInstructions_DocumentationMustBeURI(t *testing.T) {
schema := compileDraftSchema(t)

doc := baseServerWithPackagePrefix + `{
"registryType": "npm",
"identifier": "@example/server",
"version": "1.0.0",
"transport": { "type": "stdio" },
"postInstallInstructions": [
{ "description": "See docs", "documentation": "not a uri with spaces" }
]
}` + baseServerSuffix

err := validateAgainstDraft(t, schema, doc)
assert.Error(t, err, "non-URI documentation should be rejected")
}
57 changes: 57 additions & 0 deletions pkg/model/post_install_instructions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package model_test

import (
"encoding/json"
"testing"

"github.com/modelcontextprotocol/registry/pkg/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestPackage_PostInstallInstructions_RoundTrip ensures the PostInstallInstructions
// field on Package serializes and deserializes losslessly.
func TestPackage_PostInstallInstructions_RoundTrip(t *testing.T) {
pkg := model.Package{
RegistryType: "mcpb",
Identifier: "https://github.com/aniongithub/mind-map/releases/download/v1.0.0/mind-map.mcpb",
Transport: model.Transport{Type: "stdio"},
PostInstallInstructions: []model.PostInstallInstruction{
{
Description: "Install as a system service for the web UI",
Command: "mind-map service install --addr 127.0.0.1:4242",
Documentation: "https://github.com/aniongithub/mind-map#service-management",
Optional: true,
},
},
}

data, err := json.Marshal(pkg)
require.NoError(t, err)

var decoded model.Package
require.NoError(t, json.Unmarshal(data, &decoded))

require.Len(t, decoded.PostInstallInstructions, 1)
instruction := decoded.PostInstallInstructions[0]
assert.Equal(t, "Install as a system service for the web UI", instruction.Description)
assert.Equal(t, "mind-map service install --addr 127.0.0.1:4242", instruction.Command)
assert.Equal(t, "https://github.com/aniongithub/mind-map#service-management", instruction.Documentation)
assert.True(t, instruction.Optional)
}

// TestPackage_PostInstallInstructions_OmittedWhenEmpty ensures the field is omitted
// from JSON output when unset, keeping it optional.
func TestPackage_PostInstallInstructions_OmittedWhenEmpty(t *testing.T) {
pkg := model.Package{
RegistryType: "npm",
Identifier: "@example/server",
Version: "1.0.0",
Transport: model.Transport{Type: "stdio"},
}

data, err := json.Marshal(pkg)
require.NoError(t, err)

assert.NotContains(t, string(data), "postInstallInstructions")
}
17 changes: 17 additions & 0 deletions pkg/model/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,23 @@ type Package struct {
PackageArguments []Argument `json:"packageArguments,omitempty" doc:"A list of arguments to be passed to the package's binary."`
// EnvironmentVariables are set when running the package
EnvironmentVariables []KeyValueInput `json:"environmentVariables,omitempty" doc:"A mapping of environment variables to be set when running the package."`
// PostInstallInstructions are informational steps clients can surface after download
PostInstallInstructions []PostInstallInstruction `json:"postInstallInstructions,omitempty" doc:"Structured post-install steps that clients can display as a checklist after downloading the package. Intended for servers that require additional manual setup beyond installation (for example, registering a companion system service). Instructions are informational only - clients display text and never execute commands automatically."`
}

// PostInstallInstruction is a single informational post-install step that clients
// can display after downloading a package. It is purely descriptive: clients show
// the text and never execute the command automatically, so it carries no supply
// chain risk.
type PostInstallInstruction struct {
// Description is the human-readable instruction text (required).
Description string `json:"description" minLength:"1" doc:"Human-readable instruction text describing the post-install step." example:"Install as a system service for the web UI"`
// Command is an optional shell command the user can run; clients must not execute it automatically.
Command string `json:"command,omitempty" doc:"Optional shell command the user can run to complete the step. Displayed for the user to copy; clients must not execute it automatically." example:"mind-map service install --addr 127.0.0.1:4242"`
// Documentation is an optional link to docs or a setup page describing the step.
Documentation string `json:"documentation,omitempty" format:"uri" doc:"Optional link to documentation or a setup page describing the step in more detail." example:"https://github.com/aniongithub/mind-map#service-management"`
// Optional indicates whether the step is optional; when true the server works without it.
Optional bool `json:"optional,omitempty" doc:"Whether the step is optional. When true, the MCP server functions without completing this step."`
}

type Repository struct {
Expand Down
Loading