From 8eee61b8193435d4c8bea7b9ac465c2bb734d7a8 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 11 Oct 2025 18:39:51 +0200 Subject: [PATCH 1/3] feat: complete Preview2 interfaces with bidirectional pattern Add Preview2 (blocking) versions of all interfaces following the same bidirectional architecture as Preview3, plus CI workflow for validation. ## New Preview2 Interfaces - runtime.wit: Registration and framework APIs (blocking, component imports) - handlers.wit: Callback interfaces (blocking, component exports) - client.wit: Client operations (blocking, component imports) - world.wit: Three bidirectional worlds ## Architecture Preview2 follows the same bidirectional pattern as Preview3: - Component IMPORTS runtime (register capabilities, serve) - Component EXPORTS handlers (execute tools, read resources) - Host handles protocol, transport, middleware - Component handles domain logic only Key difference: All operations are blocking (no future) ## CI Workflow Added .github/workflows/ci.yml: - Validates Preview3 WIT files with Bazel - Validates Preview2 WIT files with Bazel - Checks WIT formatting with wasm-tools ## Updated Documentation - preview2/README.md: Complete architecture documentation - BUILD.bazel: Comprehensive comments explaining interfaces and worlds ## Worlds - mcp-backend-preview2: import runtime + export handlers - mcp-client-preview2: import client - mcp-proxy-preview2: import runtime + client, export handlers --- .github/workflows/ci.yml | 46 +++++++++++++ BUILD.bazel | 35 +++++++++- wit/preview2/README.md | 129 +++++++++++++++++------------------ wit/preview2/client.wit | 65 ++++++++++++++++++ wit/preview2/handlers.wit | 93 +++++++++++++++++++++++++ wit/preview2/runtime.wit | 138 ++++++++++++++++++++++++++++++++++++++ wit/preview2/world.wit | 29 ++++++++ 7 files changed, 466 insertions(+), 69 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 wit/preview2/client.wit create mode 100644 wit/preview2/handlers.wit create mode 100644 wit/preview2/runtime.wit create mode 100644 wit/preview2/world.wit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a9ade67 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + validate-wit: + name: Validate WIT Files + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.8.1 + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }} + repository-cache: true + + - name: Validate Preview3 WIT + run: bazel build //:mcp + + - name: Validate Preview2 WIT + run: bazel build //:mcp_preview2 + + check-formatting: + name: Check WIT Formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install wasm-tools + run: | + curl -LO https://github.com/bytecodealliance/wasm-tools/releases/download/v1.219.1/wasm-tools-1.219.1-x86_64-linux.tar.gz + tar xzf wasm-tools-1.219.1-x86_64-linux.tar.gz + sudo mv wasm-tools-1.219.1-x86_64-linux/wasm-tools /usr/local/bin/ + + - name: Check WIT files are valid + run: | + for witfile in wit/*.wit wit/preview2/*.wit; do + echo "Checking $witfile" + wasm-tools component wit $witfile + done diff --git a/BUILD.bazel b/BUILD.bazel index d42ec9d..cf57e9f 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -1,4 +1,13 @@ -"""WASI MCP - Model Context Protocol Interface Definitions""" +"""WASI MCP - Model Context Protocol Interface Definitions + +WASI MCP follows a bidirectional system interface pattern: +- Components IMPORT runtime (register capabilities, start serving) +- Components EXPORT handlers (execute tools, read resources) + +Architecture: + Host: Protocol, transport, auth, middleware + Component: Domain logic (tools, resources, prompts) +""" load("@rules_wasm_component//wit:defs.bzl", "wit_library") @@ -9,6 +18,17 @@ package(default_visibility = ["//visibility:public"]) # ============================================================================ # WASI MCP WIT Library - Preview3 +# +# Interfaces: +# - runtime.wit: Registration and framework APIs (component imports) +# - handlers.wit: Callback interfaces (component exports) +# - client.wit: Client operations (component imports) +# - world.wit: Bidirectional worlds +# +# Worlds: +# - mcp-backend: import runtime + export handlers +# - mcp-client: import client +# - mcp-proxy: import runtime + client, export handlers wit_library( name = "mcp", srcs = glob( @@ -27,11 +47,22 @@ wit_library( # ============================================================================ # WASI MCP WIT Library - Preview2 +# +# Interfaces (blocking versions): +# - runtime.wit: Registration and framework APIs (component imports) +# - handlers.wit: Callback interfaces (component exports) +# - client.wit: Client operations (component imports) +# - world.wit: Bidirectional worlds +# +# Worlds: +# - mcp-backend-preview2: import runtime + export handlers +# - mcp-client-preview2: import client +# - mcp-proxy-preview2: import runtime + client, export handlers wit_library( name = "mcp_preview2", srcs = glob(["wit/preview2/*.wit"]), package_name = "wasi:mcp@0.1.0-preview2", deps = [ - "@wasi_clocks_v020//:clocks", # Preview2 uses 0.2.0 + "@wasi_clocks_v020//:clocks", # Preview2 uses WASI 0.2.0 ], ) diff --git a/wit/preview2/README.md b/wit/preview2/README.md index d1ca44a..103604e 100644 --- a/wit/preview2/README.md +++ b/wit/preview2/README.md @@ -1,101 +1,96 @@ # WASI MCP Preview2 Interfaces -This directory contains **Preview2-compatible** WIT interfaces for immediate prototyping with stable WASI Preview2 toolchains. +**Status**: Blocking operations for immediate use with stable toolchains -## Why Preview2? +Preview2-compatible WIT interfaces for immediate prototyping with current stable WebAssembly toolchains. These interfaces use blocking (synchronous) operations instead of async futures. -While the main WIT interfaces in `../` use **Preview3 async patterns** (the aspirational design), Preview2 allows you to: +## Architecture -- ✅ **Build working prototypes TODAY** with stable Rust/Go/C++/JS toolchains -- ✅ **Validate the interface design** with real implementations -- ✅ **Demonstrate to stakeholders** with actual working code -- ✅ **Test the ergonomics** before Preview3 is ready +Preview2 follows the same **bidirectional pattern** as Preview3: -## Key Differences from Preview3 - -| Feature | Preview3 (../) | Preview2 (here) | -|---------|---------------|-----------------| -| **Async support** | `future>` | `result` (blocking) | -| **WASI version** | 0.2.3 | 0.2.0 | -| **Complexity** | Full MCP 2025-06-18 | Simplified for prototyping | -| **Status** | Aspirational target | Available now | -| **Use case** | Final standardization | Immediate prototyping | - -## Example: Blocking vs Async - -**Preview3 (aspirational):** -```wit -initialize: func(params: initialize-params) - -> future>; ``` - -**Preview2 (available now):** -```wit -initialize: func(params: initialize-params) - -> result; +┌─────────────────────────────────────────┐ +│ Host Runtime │ +│ - MCP Protocol Implementation │ +│ - Transport Layer (stdio/HTTP/WS) │ +│ - Middleware (auth, logging, etc.) │ +└─────────────────────────────────────────┘ + ↕ + imports runtime (blocking) + exports handlers (blocking) + ↕ +┌─────────────────────────────────────────┐ +│ Component (Your Code) │ +│ - Registration: register-server(), etc. │ +│ - Handlers: call-tool(), read-resource()│ +└─────────────────────────────────────────┘ ``` -## Building with Preview2 +**Key Principle**: Component imports runtime capabilities and exports handlers, just like Preview3, but all operations are blocking. -```bash -# Build Preview2 version -bazel build //:mcp_preview2 +## Key Differences from Preview3 -# Use in your component -wit_library( - name = "my_mcp_server", - srcs = ["server.wit"], - deps = ["@wasi_mcp//:mcp_preview2"], -) -``` +| Feature | Preview3 | Preview2 | +|---------|----------|----------| +| **Async support** | `future>` | `result` (blocking) | +| **WASI version** | 0.2.3 | 0.2.0 | +| **I/O support** | wasi:io/streams, wasi:io/poll | Not needed (blocking) | +| **Toolchain** | Requires async support | Stable toolchains | +| **Status** | Aspirational (future) | Available now | +| **Architecture** | Bidirectional (import+export) | Bidirectional (import+export) ✅ | -## Component Worlds +**Important**: Both Preview2 and Preview3 use the same bidirectional architecture. The only difference is async vs blocking operations. -### mcp-server-preview2 -Implement an MCP server with synchronous operations: +## Worlds + +### mcp-backend-preview2 +For components that provide MCP tools/resources/prompts: ```wit -world mcp-server-preview2 { +world mcp-backend-preview2 { + import runtime; // Register and serve + export handlers; // Execute tools, read resources import wasi:clocks/wall-clock@0.2.0; - export server; // Synchronous MCP operations } ``` ### mcp-client-preview2 -Build an MCP client with blocking calls: +For components that consume MCP servers: ```wit world mcp-client-preview2 { + import client; // Connect and make requests import wasi:clocks/wall-clock@0.2.0; - import server; // Make blocking MCP calls } ``` -## What's Included - -- **types.wit** - Core MCP types (simplified) -- **server.wit** - Synchronous server operations -- **world.wit** - Preview2 component worlds +### mcp-proxy-preview2 +For components that aggregate/transform MCP servers: +```wit +world mcp-proxy-preview2 { + import runtime; // Serve downstream + export handlers; // Handle downstream requests + import client; // Call upstream servers + import wasi:clocks/wall-clock@0.2.0; +} +``` -**Not included** (use Preview3 version for full spec): -- Streaming interfaces -- Complete notification system -- Full capabilities model -- Content block variants +## Building -## Migration Path +To validate Preview2 interfaces with Bazel: -1. **Now**: Prototype with Preview2 (this directory) -2. **Later**: When Preview3 is stable, migrate to `../` interfaces -3. **Eventually**: Preview3 becomes the standard +```bash +bazel build //:mcp_preview2 +``` -The core API shapes are the same, just add `future<>` wrappers when migrating. +To generate bindings: -## Examples +```bash +# Rust +wit-bindgen rust wit/preview2/ --out-dir src/bindings/ -See the examples directory for: -- Rust MCP server (Preview2) -- Go MCP client (Preview2) -- C++ MCP proxy (Preview2) +# Go +wit-bindgen-go wit/preview2/ --out-dir bindings/ +``` ## Status -✅ **Ready for prototyping** - All interfaces validated with Bazel +✅ **Ready for prototyping** - Architecture redesigned, validated with Bazel diff --git a/wit/preview2/client.wit b/wit/preview2/client.wit new file mode 100644 index 0000000..962d39c --- /dev/null +++ b/wit/preview2/client.wit @@ -0,0 +1,65 @@ +package wasi:mcp@0.1.0-preview2; + +/// MCP Client Interface (Preview2 - Blocking) +/// +/// Components import this interface to act as MCP clients. +@since(version = 0.1.0) +interface client { + use types.{error}; + use content.{content-block, prompt-message}; + + /// Tool call result from remote server + record tool-call-result { + content: list, + is-error: option, + } + + /// Resource read result from remote server + record resource-read-result { + contents: list, + } + + /// Prompt get result from remote server + record prompt-get-result { + messages: list, + } + + /// Connect to an MCP server (blocking) + @since(version = 0.1.0) + connect: func(endpoint: string) -> result; + + /// Initialize MCP session (blocking) + @since(version = 0.1.0) + initialize: func( + connection-id: string, + client-name: string, + client-version: string + ) -> result, error>; + + /// Call a tool on the remote server (blocking) + @since(version = 0.1.0) + call-tool: func( + connection-id: string, + tool-name: string, + arguments: list + ) -> result; + + /// Read a resource from the remote server (blocking) + @since(version = 0.1.0) + read-resource: func( + connection-id: string, + resource-uri: string + ) -> result; + + /// Get a prompt from the remote server (blocking) + @since(version = 0.1.0) + get-prompt: func( + connection-id: string, + prompt-name: string, + arguments: option> + ) -> result; + + /// Disconnect from an MCP server + @since(version = 0.1.0) + disconnect: func(connection-id: string) -> result<_, error>; +} diff --git a/wit/preview2/handlers.wit b/wit/preview2/handlers.wit new file mode 100644 index 0000000..33505fb --- /dev/null +++ b/wit/preview2/handlers.wit @@ -0,0 +1,93 @@ +package wasi:mcp@0.1.0-preview2; + +/// MCP Handler Interface (Preview2 - Blocking) +/// +/// Components export this interface to handle incoming MCP requests. +@since(version = 0.1.0) +interface handlers { + use types.{error}; + use content.{content-block, prompt-message}; + + /// Result of tool execution + record tool-result { + content: list, + is-error: option, + } + + /// Contents of a resource + record resource-contents { + contents: list, + } + + /// Contents of a prompt + record prompt-contents { + messages: list, + } + + /// Completion suggestion + record completion { + value: string, + label: option, + } + + /// Reference type for completion + enum completion-ref-type { + tool-name, + resource-uri, + prompt-name, + argument-name, + } + + /// Elicitation field definition + record elicitation-field { + name: string, + field-type: string, + description: option, + required: option, + } + + /// Elicitation response + record elicitation-response { + values: list, + } + + /// Called when a client requests tool execution + @since(version = 0.1.0) + call-tool: func( + name: string, + arguments: list + ) -> result; + + /// Called when a client reads a resource + @since(version = 0.1.0) + read-resource: func(uri: string) -> result; + + /// Called when a client requests a prompt + @since(version = 0.1.0) + get-prompt: func( + name: string, + arguments: option> + ) -> result; + + /// Called when a client subscribes to resource updates (optional) + @since(version = 0.1.0) + handle-subscribe: func(uri: string) -> result<_, error>; + + /// Called when a client unsubscribes from resource updates (optional) + @since(version = 0.1.0) + handle-unsubscribe: func(uri: string) -> result<_, error>; + + /// Called for auto-completion requests (optional) + @since(version = 0.1.0) + handle-complete: func( + ref-type: completion-ref-type, + ref-value: string + ) -> result, error>; + + /// Called for elicitation requests (optional) + @since(version = 0.1.0) + handle-elicit: func( + message: string, + fields: list + ) -> result; +} diff --git a/wit/preview2/runtime.wit b/wit/preview2/runtime.wit new file mode 100644 index 0000000..2f21b5a --- /dev/null +++ b/wit/preview2/runtime.wit @@ -0,0 +1,138 @@ +package wasi:mcp@0.1.0-preview2; + +/// MCP Runtime Interface (Preview2 - Blocking) +/// +/// Preview2 version with blocking operations (no future). +/// Components import this interface to register capabilities and start serving. +@since(version = 0.1.0) +interface runtime { + use types.{error, progress-token}; + use content.{log-level}; + use capabilities.{server-capabilities}; + + /// Server metadata and capabilities + record server-info { + name: string, + version: string, + capabilities: server-capabilities, + instructions: option, + } + + /// Tool definition for registration + record tool-definition { + name: string, + title: option, + description: string, + input-schema: list, + output-schema: option>, + annotations: option, + } + + /// Tool annotations for hints to clients + record tool-annotations { + title: option, + read-only-hint: option, + destructive-hint: option, + idempotent-hint: option, + open-world-hint: option, + } + + /// Resource definition for registration + record resource-definition { + uri: string, + name: string, + title: option, + description: option, + mime-type: option, + size: option, + } + + /// Resource template for dynamic URIs (RFC 6570) + record resource-template { + uri-template: string, + name: string, + title: option, + description: option, + mime-type: option, + } + + /// Prompt definition for registration + record prompt-definition { + name: string, + title: option, + description: option, + arguments: option>, + } + + /// Prompt argument definition + record prompt-argument { + name: string, + description: option, + required: option, + } + + /// Notification types that can be sent to clients + variant notification { + resources-list-changed, + resources-updated(list), + tools-list-changed, + prompts-list-changed, + progress(progress-notification), + message(message-notification), + cancelled(string), + } + + /// Progress notification details + record progress-notification { + progress-token: progress-token, + progress: u64, + total: option, + } + + /// Message notification details + record message-notification { + level: log-level, + message: string, + data: option>, + } + + /// Register this component's server information + @since(version = 0.1.0) + register-server: func(info: server-info) -> result<_, error>; + + /// Register tools this component provides + @since(version = 0.1.0) + register-tools: func(tools: list) -> result<_, error>; + + /// Register resources this component provides + @since(version = 0.1.0) + register-resources: func(resources: list) -> result<_, error>; + + /// Register resource templates (URI templates per RFC 6570) + @since(version = 0.1.0) + register-resource-templates: func(templates: list) -> result<_, error>; + + /// Register prompts this component provides + @since(version = 0.1.0) + register-prompts: func(prompts: list) -> result<_, error>; + + /// Start serving MCP requests (blocking) + @since(version = 0.1.0) + serve: func() -> result<_, error>; + + /// Send a notification to connected clients + @since(version = 0.1.0) + send-notification: func(notification: notification) -> result<_, error>; + + /// Log a message through MCP logging protocol + @since(version = 0.1.0) + log: func(level: log-level, message: string, data: option>) -> result<_, error>; + + /// Report progress for long-running operations + @since(version = 0.1.0) + report-progress: func( + token: progress-token, + progress: u64, + total: option + ) -> result<_, error>; +} diff --git a/wit/preview2/world.wit b/wit/preview2/world.wit new file mode 100644 index 0000000..a2b75ad --- /dev/null +++ b/wit/preview2/world.wit @@ -0,0 +1,29 @@ +package wasi:mcp@0.1.0-preview2; + +/// WASI MCP Worlds (Preview2 - Blocking) +/// +/// Preview2 versions with blocking operations for immediate use with stable toolchains. + +@since(version = 0.1.0) +world mcp-backend-preview2 { + import runtime; + export handlers; + import wasi:clocks/wall-clock@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; +} + +@since(version = 0.1.0) +world mcp-client-preview2 { + import client; + import wasi:clocks/wall-clock@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; +} + +@since(version = 0.1.0) +world mcp-proxy-preview2 { + import runtime; + export handlers; + import client; + import wasi:clocks/wall-clock@0.2.0; + import wasi:clocks/monotonic-clock@0.2.0; +} From 8131e9a4332a497875aecb171cfe39f65b389021 Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 11 Oct 2025 18:42:27 +0200 Subject: [PATCH 2/3] fix: update existing CI workflow instead of creating duplicate --- .github/workflows/ci.yml | 46 -------------------------------------- .github/workflows/main.yml | 21 ++++++++++++++++- 2 files changed, 20 insertions(+), 47 deletions(-) delete mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index a9ade67..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: CI - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - validate-wit: - name: Validate WIT Files - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Bazel - uses: bazel-contrib/setup-bazel@0.8.1 - with: - bazelisk-cache: true - disk-cache: ${{ github.workflow }} - repository-cache: true - - - name: Validate Preview3 WIT - run: bazel build //:mcp - - - name: Validate Preview2 WIT - run: bazel build //:mcp_preview2 - - check-formatting: - name: Check WIT Formatting - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Install wasm-tools - run: | - curl -LO https://github.com/bytecodealliance/wasm-tools/releases/download/v1.219.1/wasm-tools-1.219.1-x86_64-linux.tar.gz - tar xzf wasm-tools-1.219.1-x86_64-linux.tar.gz - sudo mv wasm-tools-1.219.1-x86_64-linux/wasm-tools /usr/local/bin/ - - - name: Check WIT files are valid - run: | - for witfile in wit/*.wit wit/preview2/*.wit; do - echo "Checking $witfile" - wasm-tools component wit $witfile - done diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 71aaf87..0e31f6f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,4 +11,23 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: WebAssembly/wit-abi-up-to-date@v19 + - uses: WebAssembly/wit-abi-up-to-date@v20 + + validate-wit: + name: Validate WIT Files + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.8.1 + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }} + repository-cache: true + + - name: Validate Preview3 WIT + run: bazel build //:mcp + + - name: Validate Preview2 WIT + run: bazel build //:mcp_preview2 From 55f5b6aa56eb590e807acf2d024c309bb935856a Mon Sep 17 00:00:00 2001 From: Ralf Anton Beier Date: Sat, 11 Oct 2025 19:04:17 +0200 Subject: [PATCH 3/3] feat: add pre-commit hooks and update README architecture docs - Add .pre-commit-config.yaml with Bazel-based WIT validation - Add .markdownlint.yaml for Markdown linting configuration - Update CI workflow to include pre-commit checks - Update README.md with bidirectional architecture documentation - Use Bazel tools for all WIT validation (no separate wasm-tools) --- .github/workflows/main.yml | 30 ++++ .markdownlint.yaml | 45 ++++++ .pre-commit-config.yaml | 61 ++++++++ README.md | 292 ++++++++++++++++++++----------------- 4 files changed, 298 insertions(+), 130 deletions(-) create mode 100644 .markdownlint.yaml create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0e31f6f..88cb1bd 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,6 +6,36 @@ on: branches: [main] jobs: + pre-commit: + name: Pre-commit checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - name: Setup Bazel + uses: bazel-contrib/setup-bazel@0.8.1 + with: + bazelisk-cache: true + disk-cache: ${{ github.workflow }} + repository-cache: true + + - name: Install pre-commit + run: pip install pre-commit + + - name: Cache pre-commit environments + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }} + + - name: Run pre-commit + run: pre-commit run --all-files --show-diff-on-failure + abi-up-to-date: name: Check ABI files are up-to-date runs-on: ubuntu-latest diff --git a/.markdownlint.yaml b/.markdownlint.yaml new file mode 100644 index 0000000..1dd8881 --- /dev/null +++ b/.markdownlint.yaml @@ -0,0 +1,45 @@ +# Markdownlint configuration for WASI MCP +# See: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md + +# Default state for all rules +default: true + +# MD013/line-length - Line length +MD013: + # Number of characters + line_length: 120 + # Number of characters for headings + heading_line_length: 120 + # Number of characters for code blocks + code_block_line_length: 120 + # Include code blocks + code_blocks: true + # Include tables + tables: true + # Include headings + headings: true + # Strict length checking + strict: false + # Stern length checking + stern: false + +# MD033/no-inline-html - Inline HTML +MD033: + # Allowed elements + allowed_elements: [] + +# MD041/first-line-heading - First line in a file should be a top-level heading +MD041: false + +# MD046/code-block-style - Code block style +MD046: + # Block style + style: fenced + +# MD049/emphasis-style - Emphasis style +MD049: + style: asterisk + +# MD050/strong-style - Strong style +MD050: + style: asterisk diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..69c858b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,61 @@ +# Pre-commit hooks for WASI MCP +# Install: pip install pre-commit && pre-commit install +# Run manually: pre-commit run --all-files + +repos: + # Standard pre-commit hooks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + name: Trim trailing whitespace + - id: end-of-file-fixer + name: Fix end of files + - id: check-yaml + name: Check YAML syntax + args: ['--unsafe'] # Allow custom tags in GitHub Actions + - id: check-added-large-files + name: Check for large files + args: ['--maxkb=1000'] + - id: check-merge-conflict + name: Check for merge conflicts + - id: mixed-line-ending + name: Fix mixed line endings + args: ['--fix=lf'] + + # Bazel validation (includes WIT validation via rules_wasm_component) + - repo: local + hooks: + - id: bazel-build-preview3 + name: Bazel build Preview3 + entry: bazel build //:mcp + language: system + files: '^(wit/.*\.wit|BUILD\.bazel|WORKSPACE|\.bazelrc|deps\.toml)$' + exclude: '^wit/preview2/.*\.wit$' + pass_filenames: false + verbose: true + + - id: bazel-build-preview2 + name: Bazel build Preview2 + entry: bazel build //:mcp_preview2 + language: system + files: '^(wit/preview2/.*\.wit|BUILD\.bazel|WORKSPACE|\.bazelrc|deps\.toml)$' + pass_filenames: false + verbose: true + + # Markdown linting + - repo: https://github.com/DavidAnson/markdownlint-cli2 + rev: v0.12.1 + hooks: + - id: markdownlint-cli2 + name: Lint Markdown files + args: ['--config', '.markdownlint.yaml'] + + # TOML validation + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + name: Format TOML files + types: [toml] + args: ['--write', '--tab-width=2'] diff --git a/README.md b/README.md index 3833fb2..6c4d592 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,35 @@ See [`wit/preview2/README.md`](wit/preview2/README.md) for details on prototypin - **AI Model Integration**: Direct LLM integration is out of scope - **Backward Compatibility**: Only targeting latest MCP spec +## Architecture + +**Bidirectional System Interface Pattern** - WASI MCP follows a clear separation of concerns: + +``` +┌─────────────────────────────────────────┐ +│ Host Runtime │ +│ - MCP Protocol Implementation │ +│ - Transport Layer (stdio/HTTP/WS) │ +│ - JSON-RPC Handling │ +│ - Middleware (auth, logging, etc.) │ +└─────────────────────────────────────────┘ + ↕ + imports runtime (async) + exports handlers (async) + ↕ +┌─────────────────────────────────────────┐ +│ Component (Your Code) │ +│ - Registration: register-server(), etc. │ +│ - Handlers: call-tool(), read-resource()│ +│ - Domain Logic Only │ +└─────────────────────────────────────────┘ +``` + +**Key Principle**: Component imports runtime capabilities and exports handlers: +- **Component IMPORTS runtime**: Register capabilities, start serving, send notifications +- **Component EXPORTS handlers**: Execute tools, read resources, get prompts +- **Host handles**: Protocol, transport, serialization, middleware + ## API Overview **Interface Structure** (following WASI multi-interface pattern): @@ -95,181 +124,184 @@ See [`wit/preview2/README.md`](wit/preview2/README.md) for details on prototypin ``` wit/ ├── deps.toml # wasi:io, wasi:clocks dependencies -├── types.wit # Core MCP types (request-id, cursor, resources, tools, prompts) +├── types.wit # Core MCP types (resources, tools, prompts) ├── capabilities.wit # ServerCapabilities, ClientCapabilities ├── content.wit # Content blocks (text, image, embedded-resource) -├── server.wit # Typed server operations (initialize, list-resources, call-tool, etc.) -├── client.wit # Typed client operations -├── notifications.wit # Notification types (list-changed, updated, progress) -├── streaming.wit # Streaming for large resources -├── world.wit # Component worlds (mcp-server, mcp-client, mcp-proxy) +├── runtime.wit # Runtime API (component imports) +├── handlers.wit # Handler interface (component exports) +├── client.wit # Client operations (component imports) +├── world.wit # Component worlds (mcp-backend, mcp-client, mcp-proxy) └── preview2/ # Preview2 version for immediate prototyping - ├── README.md # Preview2 guide + ├── README.md # Preview2 guide with bidirectional pattern ├── types.wit # Simplified types (blocking) - ├── server.wit # Synchronous server operations + ├── runtime.wit # Registration API (blocking) + ├── handlers.wit # Callback interface (blocking) + ├── client.wit # Client operations (blocking) └── world.wit # Preview2 component worlds + ``` -**Key Design**: Each MCP protocol method has a typed WIT function: +**Key Design**: Bidirectional interface pattern: ```wit -interface server { - resource mcp-server { - // Corresponds to MCP method: initialize - initialize: func(params: initialize-params) - -> future>; - - // Corresponds to MCP method: resources/list - list-resources: func(params: paginated-request, meta: option) - -> future>; - - // Corresponds to MCP method: tools/call - call-tool: func(name: string, arguments: option>, meta: option) - -> future>; +// Component IMPORTS runtime to register and serve +interface runtime { + register-server: func(info: server-info) -> future>; + register-tools: func(tools: list) -> future>; + register-resources: func(resources: list) -> future>; + serve: func() -> future>; + send-notification: func(notification: notification) -> future>; +} - // ... all other MCP methods - } +// Component EXPORTS handlers to execute operations +interface handlers { + call-tool: func(name: string, arguments: list) -> future>; + read-resource: func(uri: string) -> future>; + get-prompt: func(name: string, arguments: option>) -> future>; } ``` ## Examples -### MCP Server with Typed Operations +### MCP Backend with Bidirectional Pattern ```rust -use wasi::mcp::{types::*, server::*, capabilities::*}; +use wasi::mcp::{types::*, runtime::*, handlers::*, capabilities::*}; +// Component initialization: Register with runtime #[export] -fn create_database_server() -> Result { - let implementation = Implementation { +async fn init() -> Result<(), Error> { + let server_info = ServerInfo { name: "database-context".to_string(), version: "1.0.0".to_string(), - website_url: None, + capabilities: ServerCapabilities { + resources: Some(ResourcesCapability { + subscribe: Some(true), + list_changed: Some(true), + }), + tools: Some(ToolsCapability { + list_changed: Some(false), + }), + prompts: None, + logging: None, + }, + instructions: Some("Database context provider for SQL queries".to_string()), }; - let capabilities = ServerCapabilities { - resources: Some(ResourcesCapability { - subscribe: Some(true), - list_changed: Some(true), - }), - tools: Some(ToolsCapability { - list_changed: Some(false), - }), - prompts: None, - logging: None, - sampling: None, - elicitation: None, - completions: None, - experimental: None, - }; - - create_server(implementation, capabilities) + // Register server with runtime (component imports runtime) + register_server(server_info).await?; + + // Register tools + let tools = vec![ + ToolDefinition { + name: "execute-query".to_string(), + description: Some("Execute SQL query".to_string()), + input_schema: tool_schema(), + }, + ]; + register_tools(tools).await?; + + // Register resources + let resources = vec![ + ResourceDefinition { + uri: "db://tables".to_string(), + name: "database-tables".to_string(), + description: Some("List of all database tables".to_string()), + mime_type: Some("application/json".to_string()), + }, + ]; + register_resources(resources).await?; + + // Start serving + serve().await } -// Implement typed server operations -impl mcp_server { - async fn initialize(&self, params: InitializeParams) - -> Result - { - // Perform version negotiation - let protocol_version = if params.protocol_version == "2025-06-18" { - "2025-06-18".to_string() - } else { - "2025-03-26".to_string() // Fallback - }; - - Ok(InitializeResult { - protocol_version, - capabilities: self.get_capabilities(), - server_info: self.get_implementation(), - instructions: Some("Database context provider for SQL queries".to_string()), - }) - } - - async fn list_resources(&self, params: PaginatedRequest, meta: Option) - -> Result - { - let resources = vec![ - Resource { - uri: "db://tables".to_string(), - name: "database-tables".to_string(), - title: Some("Database Tables".to_string()), - description: Some("List of all database tables".to_string()), - mime_type: Some("application/json".to_string()), - size: None, - annotations: None, - }, - ]; - - Ok(ListResourcesResult { - resources, - next_cursor: None, - }) +// Component exports handlers for runtime to call +#[export] +async fn call_tool(name: String, arguments: Vec) -> Result { + match name.as_str() { + "execute-query" => { + let args: QueryArgs = serde_json::from_slice(&arguments)?; + let result = execute_database_query(&args.query).await?; + + Ok(ToolResult { + content: vec![ContentBlock::Text(TextContent { + content_type: "text".to_string(), + text: serde_json::to_string(&result)?, + annotations: None, + })], + is_error: Some(false), + }) + } + _ => Err(Error::tool_not_found(format!("Unknown tool: {}", name))) } +} - async fn call_tool(&self, name: String, arguments: Option>, meta: Option) - -> Result - { - match name.as_str() { - "execute-query" => { - let args: QueryArgs = serde_json::from_slice(&arguments.unwrap())?; - let result = execute_database_query(&args.query).await?; - - Ok(CallToolResult { - content: vec![ContentBlock::Text(TextContent { - content_type: "text".to_string(), - text: serde_json::to_string(&result)?, - annotations: None, - })], - structured_content: Some(serde_json::to_vec(&result)?), - is_error: Some(false), - }) - } - _ => Err(Error::tool_not_found(format!("Unknown tool: {}", name))) +#[export] +async fn read_resource(uri: String) -> Result { + match uri.as_str() { + "db://tables" => { + let tables = get_database_tables().await?; + Ok(ResourceContents { + contents: vec![ContentBlock::Text(TextContent { + content_type: "text".to_string(), + text: serde_json::to_string(&tables)?, + annotations: None, + })], + }) } + _ => Err(Error::resource_not_found(format!("Unknown resource: {}", uri))) } } ``` -### MCP Client with Typed Operations +### MCP Client Operations ```rust use wasi::mcp::{client::*, types::*}; #[export] -async fn query_mcp_server() -> Result, Error> { - let client_impl = Implementation { - name: "ai-agent".to_string(), - version: "1.0.0".to_string(), - website_url: None, - }; +async fn query_mcp_server() -> Result, Error> { + // Connect to remote MCP server (component imports client) + let connection_id = connect("http://localhost:3000/mcp").await?; + + // Initialize session + let (server_name, server_version) = initialize( + connection_id.clone(), + "ai-agent".to_string(), + "1.0.0".to_string() + ).await?; - let capabilities = ClientCapabilities { - roots: None, - sampling: None, - elicitation: None, - experimental: None, - }; + println!("Connected to {} v{}", server_name, server_version); - let client = create_client(client_impl, capabilities)?; + // Call a tool on the remote server + let tool_args = serde_json::to_vec(&QueryArgs { + query: "SELECT * FROM users".to_string() + })?; - // Initialize connection - let init_params = InitializeParams { - protocol_version: "2025-06-18".to_string(), - capabilities, - client_info: client_impl, - }; + let result = call_tool( + connection_id.clone(), + "execute-query".to_string(), + tool_args + ).await?; - let init_result = client.initialize(init_params).await?; - client.send_initialized()?; + // Read a resource + let resource_result = read_resource( + connection_id.clone(), + "db://tables".to_string() + ).await?; - // List available resources - let resources_result = client.list_resources( - PaginatedRequest { cursor: None }, + // Get a prompt + let prompt_result = get_prompt( + connection_id.clone(), + "sql-template".to_string(), None ).await?; - Ok(resources_result.resources) + // Clean up + disconnect(connection_id).await?; + + Ok(result.content[0].as_bytes().to_vec()) } ```