feat: refine representation of commands in the API

Refs #28. After some thinking, this tries for a 'best of both worlds'
approach. Simple arguments can be present essentially as the original
spec recommended. As discussed, we don't specify the args for `npx` and
friends directly, so a simple package is merely...

```yaml
- name: my-api-mcp
  # ...
  packages:
    - type: npm
      name: "@c4312/my-api-mcp"
      version: "0.0.1"
      arguments:
        # Named inputs in 'arguments' generate a `--name=<value>`
        - type: named
          name: "api-key"
          is_required: true
          is_secret: true
```

However, I think we need to be able to specify runtime args in some
cases, otherwise the registry eventually mirrors all docker configs and
options which is not a great situation to be in. So, I suggest a
`runtime_args` in the same format. Say I wanted that for npm, I could:

```yaml
- name: my-api-mcp
  # ...
  packages:
    - type: npm
      name: "@c4312/my-api-mcp"
      version: "0.0.1"
      runtime_arguments:
        # Arguments with a pre-specified `value` don't get prompted for input
        - type: positional
          value: "--experimental-eventsource"
```

But Docker mounts are... complex, and ultimately we may deal with cases
that need multiple inputs in an argument like that. So for that I suggest
the third kind of "templated" input that does basic replacements and has
`properties` that follow the same general input schema:

```yaml
- name: server-filesystem
  # ...
  packages:
    - type: docker
      name: "mcp/filesystem"
      version: "0.0.1"
      runtime_arguments:
        - type: template
          description: Mount paths to access.
          required: true
          repated: true
          template: "--mount=type=bind,src={host_path},dst={container_path}"
          values:
            host_path:
              description: Path to access on the local machine
              required: true
              format: filepath
            container_path:
              description: Path to mount in the container. It should be rooted in `/project` directory.
              required: true
              format: string
              default: "/project"

      arguments:
        - type: positional
          value: "/project"
```

And then client config ends up being pretty simple, for the above case
for example a client could have this config:

```
{
  "filesystem": {
    "server": "@modelcontextprotocol/servers/src/filesystem@1.0.2",
    "package": "docker",
    "settings": {
      "mount_config": [
        { "source_path": "/Users/username/Desktop", "target_path": "/project/desktop" },
        { "source_path": "/path/to/other/allowed/dir", "target_path": "//projects/other/allowed/dir,ro" },
      ]
    }
  }
}
```

Which might generate a CLI:

```
docker run \
  --mount=type=bind,src=/Users/username/Desktop,dst=/project/desktop \
  --mount=type=bind,src=/path/to/other/allowed/dir,dst=/project/other/allowed/dir,ro \
  mcp/filesystem:1.0.2 \
  /project
```

I think this does a good job of accomplishing the goals we care about:

- We get nicely defined inputs that are easy to extend, easy to process,
  and easy for clients to configure.
- Versioning is enforced as we don't require the package to specify the
  identifier to npx/uvx/etc.
- We _usually_ can break away from a dependency on a specific runtime.
  This promise is broken a bit with `runtime_arguments`, most packages
  should not need this and those that do are those inherently more
  tightly coupled to a specific runtime like Docker (e.g. they would be
  pretty uncommon for Node or Python.)
- We don't have a suple complex template system with loops and fallbacks
  and conditionals, like we would if we tried to have a single template
  string for a command.
This commit is contained in:
Connor Peet 2025-05-09 18:37:08 -07:00
commit 8e5cf14a42
2 changed files with 194 additions and 306 deletions

View file

@ -65,122 +65,61 @@ GET /v0/servers/a5e8a7f0-d4e4-4a1d-b12f-2896a23fd4f1?version=0.0.3
"registry_name": "npm", "registry_name": "npm",
"name": "@modelcontextprotocol/server-filesystem", "name": "@modelcontextprotocol/server-filesystem",
"version": "1.0.2", "version": "1.0.2",
"command": { "arguments": [
"name": "npx", {
"subcommands": [], "type": "positional",
"positional_arguments": [ "description": "Path to access",
{ "default": "/Users/username/Desktop",
"position": 0, "is_required": true,
"name": "package", "is_repeated": true
"description": "NPM package name", }
"default_value": "@modelcontextprotocol/server-filesystem", ],
"is_required": true,
"is_editable": false,
"is_repeatable": false,
"choices": []
},
{
"position": 1,
"name": "path",
"description": "Path to access",
"default_value": "/Users/username/Desktop",
"is_required": true,
"is_editable": true,
"is_repeatable": true,
"choices": []
}
],
"named_arguments": [
{
"short_flag": "-y",
"requires_value": false,
"is_required": false,
"is_editable": false,
"description": "Skip prompts and automatically answer yes",
"choices": []
}
]
},
"environment_variables": [ "environment_variables": [
{ {
"type": "named",
"name": "LOG_LEVEL", "name": "LOG_LEVEL",
"description": "Logging level (debug, info, warn, error)", "description": "Logging level (debug, info, warn, error)",
"required": false, "default": "info"
"default_value": "info"
} }
] ]
}, },
{ {
"name": "docker", "registry_name": "docker",
"package_name": "mcp/filesystem", "name": "mcp/filesystem",
"version": "1.0.2", "version": "1.0.2",
"command": { "runtime_arguments": [
"name": "docker", {
"subcommands": [ "type": "template",
{ "description": "Mount a volume into the container",
"name": "run", "template": "--mount=type=bind,src={source_path},dst={target_path}",
"description": "Run the Docker container", "is_required": true,
"is_required": true, "is_repeated": true,
"subcommands": [], "properties": {
"positional_arguments": [], "source_path": {
"named_arguments": [ "description": "Source path on host",
{ "format": "filepath",
"short_flag": "-i", "is_required": true
"requires_value": false, },
"is_required": true, "target_path": {
"is_editable": false, "description": "Path to mount in the container. It should be rooted in `/project` directory.",
"description": "Run in interactive mode" "is_required": true,
}, "default": "/project",
{ }
"long_flag": "--rm",
"requires_value": false,
"is_required": true,
"is_editable": false,
"description": "Remove container when it exits"
},
{
"long_flag": "--mount",
"requires_value": true,
"is_required": true,
"is_repeatable": true,
"is_editable": true,
"description": "Mount a volume into the container",
"default_value": "type=bind,src=/Users/username/Desktop,dst=/projects/Desktop",
"choices": []
}
]
} }
], }
"positional_arguments": [ ],
{ "arguments": [
"position": 0, {
"name": "image", "type": "positional",
"description": "Docker image name", "value": "/project",
"default_value": "mcp/filesystem", }
"is_required": true, ]
"is_editable": false,
"is_repeatable": false,
"choices": []
},
{
"position": 1,
"name": "root_path",
"description": "Root path for filesystem access",
"default_value": "/projects",
"is_required": true,
"is_editable": false,
"is_repeatable": false,
"choices": []
}
],
"named_arguments": []
},
"environment_variables": [ "environment_variables": [
{ {
"type": "named",
"name": "LOG_LEVEL", "name": "LOG_LEVEL",
"description": "Logging level (debug, info, warn, error)", "description": "Logging level (debug, info, warn, error)",
"required": false, "default": "info"
"default_value": "info"
} }
] ]
} }
@ -220,25 +159,13 @@ API Response:
"registry_name": "npm", "registry_name": "npm",
"name": "@modelcontextprotocol/server-brave-search", "name": "@modelcontextprotocol/server-brave-search",
"version": "1.0.2", "version": "1.0.2",
"command": {
"name": "npx",
"subcommands": [],
"positional_arguments": [],
"named_arguments": [
{
"short_flag": "-y",
"requires_value": false,
"is_required": false,
"description": "Skip prompts"
}
]
},
"environment_variables": [ "environment_variables": [
{ {
"type": "named",
"name": "BRAVE_API_KEY", "name": "BRAVE_API_KEY",
"description": "Brave Search API Key", "description": "Brave Search API Key",
"required": true, "is_required": true,
"default_value": "" "is_secret": true
} }
] ]
} }
@ -286,51 +213,33 @@ API Response:
"registry_name": "docker", "registry_name": "docker",
"name": "mcp/filesystem", "name": "mcp/filesystem",
"version": "1.0.2", "version": "1.0.2",
"command": { "runtime_arguments": [
"name": "docker", {
"subcommands": [ "type": "template",
{ "description": "Mount a volume into the container",
"name": "run", "template": "--mount=type=bind,src={source_path},dst={target_path}",
"description": "Run the Docker container", "is_required": true,
"is_required": true, "is_repeated": true,
"named_arguments": [ "properties": {
{ "source_path": {
"short_flag": "-i", "description": "Source path on host",
"requires_value": false, "format": "filepath",
"is_required": true, "is_required": true
"description": "Run in interactive mode" },
}, "target_path": {
{ "description": "Path to mount in the container. It should be rooted in `/project` directory.",
"long_flag": "--rm", "is_required": true,
"requires_value": false, "default": "/project",
"is_required": true, }
"description": "Remove container when it exits"
},
{
"long_flag": "--mount",
"requires_value": true,
"is_required": true,
"is_repeatable": true,
"description": "Mount a volume into the container"
}
]
} }
], }
"positional_arguments": [ ],
{ "arguments": [
"position": 0, {
"name": "image", "type": "positional",
"description": "Docker image name", "value": "/project",
"default_value": "mcp/filesystem" }
}, ]
{
"position": 1,
"name": "root_path",
"description": "Root path for filesystem access",
"default_value": "/projects"
}
]
}
} }
] ]
} }
@ -340,17 +249,14 @@ claude_desktop_config.json:
```json ```json
{ {
"filesystem": { "filesystem": {
"command": "docker", "server": "@modelcontextprotocol/servers/src/filesystem@1.0.2",
"args": [ "package": "docker",
"run", "settings": {
"-i", "mount_config": [
"--rm", { "source_path": "/Users/username/Desktop", "target_path": "/project/desktop" },
"--mount", "type=bind,src=/Users/username/Desktop,dst=/projects/Desktop", { "source_path": "/path/to/other/allowed/dir", "target_path": "/project/other/allowed/dir,ro" },
"--mount", "type=bind,src=/path/to/other/allowed/dir,dst=/projects/other/allowed/dir,ro", ]
"--mount", "type=bind,src=/path/to/file.txt,dst=/projects/path/to/file.txt", }
"mcp/filesystem",
"/projects"
]
} }
} }
``` ```
@ -380,4 +286,4 @@ API Response:
} }
] ]
} }
``` ```

View file

@ -3,8 +3,8 @@ info:
title: MCP Server Registry API title: MCP Server Registry API
summary: API for discovering and accessing MCP servers metadata summary: API for discovering and accessing MCP servers metadata
description: | description: |
REST API that centralizes metadata about publicly available MCP servers by allowing server creators to submit REST API that centralizes metadata about publicly available MCP servers by allowing server creators to submit
and maintain metadata about their servers in a standardized format. This API enables MCP client and maintain metadata about their servers in a standardized format. This API enables MCP client
applications and "server aggregator" type consumers to discover and install MCP servers. applications and "server aggregator" type consumers to discover and install MCP servers.
version: 0.0.1 version: 0.0.1
contact: contact:
@ -82,7 +82,7 @@ paths:
example: "Server not found" example: "Server not found"
components: components:
schemas: schemas:
jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema" jsonSchemaDialect: "https://json-schema.org/draft/2020-12/schema"
Repository: Repository:
type: object type: object
required: required:
@ -101,7 +101,7 @@ components:
id: id:
type: string type: string
example: "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9" example: "b94b5f7e-c7c6-d760-2c78-a5e9b8a5b8c9"
Server: Server:
type: object type: object
required: required:
@ -144,7 +144,7 @@ components:
example: true example: true
description: Whether the MCP server version is the latest version available in the registry. description: Whether the MCP server version is the latest version available in the registry.
$schema: "https://json-schema.org/draft/2020-12/schema" $schema: "https://json-schema.org/draft/2020-12/schema"
ServerList: ServerList:
type: object type: object
required: required:
@ -162,131 +162,7 @@ components:
total_count: total_count:
type: integer type: integer
example: 1 example: 1
Argument:
type: object
required:
- name
- description
- is_required
properties:
name:
type: string
example: "path"
description:
type: string
example: "Path to access"
is_required:
type: boolean
example: true
is_repeatable:
type: boolean
example: false
is_editable:
type: boolean
example: true
choices:
type: array
items:
type: string
example: []
default_value:
type: string
example: "/Users/username/Desktop"
PositionalArgument:
allOf:
- $ref: '#/components/schemas/Argument'
- type: object
required:
- position
properties:
position:
type: integer
example: 1
NamedArgument:
allOf:
- $ref: '#/components/schemas/Argument'
- type: object
properties:
short_flag:
type: string
example: "-y"
long_flag:
type: string
example: "--mount"
requires_value:
type: boolean
example: true
Subcommand:
type: object
required:
- name
- description
properties:
name:
type: string
example: "run"
description:
type: string
example: "Run the Docker container"
is_required:
type: boolean
example: true
subcommands:
type: array
items:
$ref: '#/components/schemas/Subcommand'
positional_arguments:
type: array
items:
$ref: '#/components/schemas/PositionalArgument'
named_arguments:
type: array
items:
$ref: '#/components/schemas/NamedArgument'
Command:
type: object
properties:
name:
type: string
enum: [npx, docker, pypi, uvx] # TODO: Add all supported commands as a whitelist
example: "npx"
subcommands:
type: array
items:
$ref: '#/components/schemas/Subcommand'
positional_arguments:
type: array
items:
$ref: '#/components/schemas/PositionalArgument'
named_arguments:
type: array
items:
$ref: '#/components/schemas/NamedArgument'
EnvironmentVariable:
type: object
required:
- name
- description
properties:
name:
type: string
example: "LOG_LEVEL"
description:
type: string
example: "Logging level (debug, info, warn, error)"
required:
type: boolean
example: false
default_value:
type: string
example: "info"
Package: Package:
type: object type: object
required: required:
@ -304,13 +180,115 @@ components:
version: version:
type: string type: string
example: "1.0.2" example: "1.0.2"
command: runtime_arguments:
$ref: '#/components/schemas/Command' type: array
description: List of arguments passed to the package's runtime command (such as docker or npx).
items:
$ref: '#/components/schemas/Input'
arguments:
type: array
description: List of arguments passed to the package's binary.
items:
$ref: '#/components/schemas/Input'
environment_variables: environment_variables:
type: array type: array
items: items:
$ref: '#/components/schemas/EnvironmentVariable' $ref: '#/components/schemas/NamedInput'
UserInput:
type: object
properties:
description:
type: string
is_required:
type: boolean
default: false
is_repeated:
type: boolean
description: Whether the input can be repeated multiple times in the command line.
default: false
format:
type: string
description: Input format. Includes a variety of standardized formats, including `filepath`, which should be interpreted as a file on the user's filesystem.
enum: [string, number, boolean, array, filepath]
default: string
value:
type: string
description: The default value for the input. If this is not set, the user will be asked to provide a value.
is_secret:
type: boolean
description: Whether the input is a secret value (e.g., password, token). If true, clients should handle the value securely.
default: false
default:
type: string
description: The default value for the input.
PositionalInput:
description: A positional input is a value inserted verbatim into the command line.
allOf:
- $ref: '#/components/schemas/UserInput'
- type: object
required:
- type
properties:
type:
type: string
enum: [positional]
example: "positional"
NamedInput:
description: A command-line `--flag={value}`, named environment variable or header.
allOf:
- $ref: '#/components/schemas/UserInput'
- type: object
required:
- type
- name
properties:
type:
type: string
enum: [named]
example: "named"
name:
type: string
example: "--port"
TemplateInput:
- type: object
required:
- type
- template
- name
properties:
type:
type: string
enum: [template]
name:
type: string
example: mount_config
description: The name of the input, which can be used by clients as an identifier to reference the input. It is not present in the generated command line.
description:
type: string
is_required:
type: boolean
is_repeated:
type: boolean
description: Whether the input can be repeated multiple times in the command line.
template:
type: string
description: The template string that will be used to generate the value. Values wrapped in `{curly_braces}` will be replaced with the corresponding properties.
example: "--mount=type=bind,src={host_path},dst={container_path}"
properties:
type: object
additionalProperties:
$ref: '#/components/schemas/UserInput'
Input:
anyOf:
- $ref: '#/components/schemas/PositionalInput'
- $ref: '#/components/schemas/NamedInput'
- $ref: '#/components/schemas/TemplateInput'
Remote: Remote:
type: object type: object
required: required:
@ -325,7 +303,11 @@ components:
type: string type: string
format: uri format: uri
example: "https://mcp-fs.example.com/sse" example: "https://mcp-fs.example.com/sse"
headers:
type: array
items:
$ref: '#/components/schemas/NamedInput'
ServerDetail: ServerDetail:
allOf: allOf:
- $ref: '#/components/schemas/Server' - $ref: '#/components/schemas/Server'
@ -342,4 +324,4 @@ components:
remotes: remotes:
type: array type: array
items: items:
$ref: '#/components/schemas/Remote' $ref: '#/components/schemas/Remote'