#
tokens: 9263/50000 30/30 files
lines: off (toggle) GitHub
raw markdown copy
# Directory Structure

```
├── .changeset
│   ├── commit.cjs
│   ├── config.json
│   ├── full-parents-draw.md
│   ├── honest-bobcats-speak.md
│   ├── README.md
│   └── thirty-melons-jog.md
├── .github
│   └── workflows
│       ├── build-cli.yml
│       ├── format.yml
│       └── release.yml
├── .gitignore
├── bun.lock
├── bunfig.toml
├── cmd
│   └── cli.go
├── download_cli.sh
├── examples
│   ├── airtabl3
│   │   ├── index.ts
│   │   ├── lib
│   │   │   └── index.ts
│   │   └── package.json
│   ├── echo
│   │   ├── bun.lock
│   │   ├── index.ts
│   │   └── package.json
│   └── systeminfo
│       ├── bun.lock
│       ├── index.ts
│       ├── lib
│       │   └── info.ts
│       └── package.json
├── expose.go
├── go.mod
├── go.sum
├── makefile
├── package.json
├── packages
│   └── expose
│       ├── CHANGELOG.md
│       ├── package.json
│       ├── src
│       │   ├── index.ts
│       │   ├── mcp.ts
│       │   └── tool.ts
│       └── tsconfig.json
├── README.md
└── scripts
    ├── format
    └── publish
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
# dependencies (bun install)
node_modules

# output
out
dist
*.tgz

# code coverage
coverage
*.lcov

# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json

# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local

# caches
.eslintcache
.cache
*.tsbuildinfo

# IntelliJ based IDEs
.idea

# Finder (MacOS) folder config
.DS_Store

tmp/

```

--------------------------------------------------------------------------------
/.changeset/README.md:
--------------------------------------------------------------------------------

```markdown
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
---
title: Expose
description: Easily build MCP tools for Claude desktop app
---

Expose lets you build MCP tools that you can invoke with MCP client like Claude desktop app.

- **Self-hosted**: You can easily self-host tools and deploy them on your own server.
- **Unified gateway**: Generates a single HTTP endpoint that you can register with `expose-cli`
- **Flexible**: Easily configure and customize your tools to fit your needs.

## Getting started

1. **Setup expose CLI**

   ```bash
   curl -fsSL https://github.com/a0dotrun/expose/releases/download/stable/download_cli.sh | bash
   ```

2. **Install dependencies**

   ```bash
   npm i @a0dotrun/expose
   ```

3. **Create server**

   ```bash
   touch src/server.ts
   ```

   ```ts title=src/server.ts
   import { create } from "@a0dotrun/expose"

   const app = create({
     tools: [],
   })

   export default {
     port: 3000,
     fetch: app.fetch,
   }
   ```

4. **Define your tools**

```diff lang=ts title=src/server.ts
+ import { tool } from "@a0dotrun/expose/tool"
+ import { subscription } from "@acme/lib/subscription"
+ import { z } from "zod"

+ const getCustomerSubscription = tool({
+   name: "getCustomerSubscription",
+   description: "Get subscription information for a customer",
+   args: z.object({
+       customer: z.string().uuid()
+   }),
+   run: async (input) => {
+     // Your subscription logic here
+     return input;
+   },
+ });


+ const createCustomerSubscription = tool({
+   name: "createCustomerSubscription",
+   description: "Create a subscription for a customer",
+   args: z.object({
+       customer: z.string().uuid()
+   }),
+   run: async (input) => {
+     // Your subscription logic here
+     return input;
+   },
+ });

const app = create({
  tools: [
+    getCustomerSubscription,
+    createCustomerSubscription,
  ],
});
```

5. **Start server**

   ```bash
   npm run dev
   ```

   You can also deploy the server and note down the public URL.

6. **Register in Claude desktop app**
   MACOS Claude desktop MCP config path: `~/Library/Application Support/Claude/claude_desktop_config.json`

```json
{
  ...
  "mcpServers": {
    "subscriptionManager": {
      "command": "/Users/acmeuser/.local/bin/expose-cli",
      "args": ["--url", "http://localhost:3000", "--timeout", "15"]
    }
  }
  ...
}
```

_replace localhost with your public URL_

---

expose is created by [@\_sanchitrk](https://x.com/_sanchitrk) at [a0](https://a0.run)

### Acknowledgements

I was inspired by [opencontrol](https://github.com/toolbeam/opencontrol)

```

--------------------------------------------------------------------------------
/bunfig.toml:
--------------------------------------------------------------------------------

```toml
[install]
exact = true
```

--------------------------------------------------------------------------------
/.changeset/thirty-melons-jog.md:
--------------------------------------------------------------------------------

```markdown
---
"@a0dotrun/expose": patch
---

tweaks

```

--------------------------------------------------------------------------------
/.changeset/full-parents-draw.md:
--------------------------------------------------------------------------------

```markdown
---
"@a0dotrun/expose": patch
---

updated install script

```

--------------------------------------------------------------------------------
/.changeset/honest-bobcats-speak.md:
--------------------------------------------------------------------------------

```markdown
---
"@a0dotrun/expose": patch
---

fixes install expose-cli script

```

--------------------------------------------------------------------------------
/examples/echo/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@expose/example-echo",
  "private": true,
  "version": "0.0.0",
  "dependencies": {
    "@a0dotrun/expose": "workspace:*"
  }
}

```

--------------------------------------------------------------------------------
/examples/systeminfo/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@expose/example-systeminfo",
  "private": true,
  "version": "0.0.0",
  "dependencies": {
    "@a0dotrun/expose": "workspace:*"
  }
}

```

--------------------------------------------------------------------------------
/.changeset/commit.cjs:
--------------------------------------------------------------------------------

```
/** @type {import('@changesets/types').CommitFunctions["getAddMessage"]} */
module.exports.getAddMessage = async (changeset) => {
  return changeset.summary;
};

```

--------------------------------------------------------------------------------
/examples/airtabl3/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "@expose/example-airtabl3",
  "private": true,
  "version": "0.0.0",
  "dependencies": {
    "@a0dotrun/expose": "workspace:*",
    "airtable": "0.12.2"
  }
}

```

--------------------------------------------------------------------------------
/packages/expose/tsconfig.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://json.schemastore.org/tsconfig",
  "extends": "@tsconfig/node22/tsconfig.json",
  "compilerOptions": {
    "outDir": "dist",
    "declaration": true
  }
}

```

--------------------------------------------------------------------------------
/packages/expose/CHANGELOG.md:
--------------------------------------------------------------------------------

```markdown
# expose

## 0.0.4

### Patch Changes

- 3a6f233: gh tweaks

## 0.0.3

### Patch Changes

- a8abced: fix package naming

## 0.0.2

### Patch Changes

- 35fe442: v0 working example with examples

```

--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": "./commit.cjs",
  "fixed": [["@a0dotrun/expose"]],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": ["@expose/example-*"]
}

```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
{
  "name": "expose",
  "private": true,
  "type": "module",
  "packageManager": "bun",
  "workspaces": [
    "packages/*",
    "examples/*"
  ],
  "devDependencies": {
    "@changesets/cli": "2.28.1",
    "@tsconfig/node22": "22.0.0",
    "@types/node": "22.13.9",
    "prettier": "3.5.3",
    "typescript": "^5"
  },
  "prettier": {
    "semi": false
  }
}

```

--------------------------------------------------------------------------------
/examples/systeminfo/lib/info.ts:
--------------------------------------------------------------------------------

```typescript
import os from "os"

export function serverInfo() {
  return {
    hostname: os.hostname(),
    platform: os.platform(),
    arch: os.arch(),
    uptime: os.uptime(),
    totalMemory: os.totalmem(),
    freeMemory: os.freemem(),
    cpus: os.cpus().map((cpu) => ({
      model: cpu.model,
      speed: cpu.speed,
    })),
    networkInterfaces: os.networkInterfaces(),
    loadAvg: os.loadavg(),
  }
}

```

--------------------------------------------------------------------------------
/examples/echo/index.ts:
--------------------------------------------------------------------------------

```typescript
import { create } from "@a0dotrun/expose"
import { tool } from "@a0dotrun/expose/tool"
import { z } from "zod"

const echo = tool({
  name: "echo",
  description: "echoes the message back",
  args: z.object({
    message: z.string(),
  }),
  async run(args) {
    return "Echo from server: " + args.message
  },
})

const app = create({
  tools: [echo],
})

export default {
  port: 3000,
  fetch: app.fetch,
}

```

--------------------------------------------------------------------------------
/examples/systeminfo/index.ts:
--------------------------------------------------------------------------------

```typescript
import { create } from "@a0dotrun/expose"
import { tool } from "@a0dotrun/expose/tool"
import { serverInfo } from "./lib/info"

const systemInfo = tool({
  name: "systemInfo",
  description:
    "Get sytem information about the server like uptime, memory, cpu, etc.",
  run: async () => {
    return serverInfo()
  },
})

const app = create({
  tools: [systemInfo],
})

export default {
  port: 3000,
  fetch: app.fetch,
}

```

--------------------------------------------------------------------------------
/.github/workflows/format.yml:
--------------------------------------------------------------------------------

```yaml
name: Format and Lint

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

jobs:
  format:
    runs-on: ubuntu-latest

    permissions:
      contents: write
      pull-requests: write

    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.head_ref }}
          fetch-depth: 0
      - uses: oven-sh/setup-bun@v2
      - run: |
          git config --local user.email "[email protected]"
          git config --local user.name "gh-actions-bot"
          make format

```

--------------------------------------------------------------------------------
/packages/expose/package.json:
--------------------------------------------------------------------------------

```json
{
  "$schema": "https://json.schemastore.org/package.json",
  "name": "@a0dotrun/expose",
  "version": "0.0.4",
  "type": "module",
  "scripts": {
    "build": "bun tsc"
  },
  "exports": {
    ".": "./dist/index.js",
    "./*": "./dist/*.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "1.6.1",
    "@tsconfig/bun": "1.0.7",
    "hono": "4.7.4",
    "zod": "3.24.2",
    "zod-to-json-schema": "3.24.3"
  },
  "devDependencies": {
    "@standard-schema/spec": "1.0.0"
  },
  "publishConfig": {
    "access": "public"
  }
}

```

--------------------------------------------------------------------------------
/packages/expose/src/tool.ts:
--------------------------------------------------------------------------------

```typescript
import { StandardSchemaV1 } from "@standard-schema/spec"
import { z } from "zod"

export interface Tool<
  Args extends undefined | StandardSchemaV1 = undefined | StandardSchemaV1,
> {
  name: string
  description: string
  args?: Args
  run: Args extends StandardSchemaV1
    ? (args: StandardSchemaV1.InferOutput<Args>) => Promise<any>
    : () => Promise<any>
}

export function tool<Args extends undefined | StandardSchemaV1>(
  input: Tool<Args>,
) {
  return input
}

// Example

tool({
  name: "echo",
  description: "echoes the message back",
  args: z.object({
    message: z.string(),
  }),
  async run(args) {
    return args.message
  },
})

```

--------------------------------------------------------------------------------
/packages/expose/src/index.ts:
--------------------------------------------------------------------------------

```typescript
import { Hono } from "hono"
// import { HTTPException } from "hono/http-exception"
import { Tool } from "./tool.js"
import { createMcp } from "./mcp.js"

export interface ExposeOptions {
  token?: string
}

export function create(input: { tools: Tool[]; key?: string }) {
  const mcp = createMcp({ tools: input.tools })

  return (
    new Hono()
      // .use((c, next) => {
      //   if (input?.key) {
      //     const authorization = c.req.header("Authorization")
      //     if (authorization !== `Bearer ${input?.key}`) {
      //       throw new HTTPException(401)
      //     }
      //   }
      //   return next()
      // })
      .post("/", async (c) => {
        const body = await c.req.json()
        console.log("mcp request", body)
        const result = await mcp.process(body)
        console.log("mcp response", result)
        return c.json(result)
      })
  )
}

```

--------------------------------------------------------------------------------
/cmd/cli.go:
--------------------------------------------------------------------------------

```go
package main

import (
	"fmt"
	"log"
	"os"
	"time"

	"github.com/a0dotrun/expose"
	"github.com/spf13/cobra"
)

func main() {
	var url string
	var timeout int

	rootCmd := &cobra.Command{
		Use:   "expose-cli",
		Short: "A CLI tool for proxying MCP tools",
		Long:  `expose-cli is a command-line tool that proxies MCP tools to a specified URL with configurations`,
		Run: func(cmd *cobra.Command, args []string) {
			if url == "" {
				fmt.Println("Error: URL is required")
				cmd.Help()
				os.Exit(1)
			}

			timeoutDuration := time.Duration(timeout) * time.Second
			if err := expose.ProxyServeStdio(url, timeoutDuration); err != nil {
				log.Fatalf("Failed to start proxy server: %v", err)
			}
		},
	}

	// Define flags
	rootCmd.Flags().StringVar(&url, "url", "", "Target URL to proxy (required)")
	rootCmd.Flags().IntVar(&timeout, "timeout", 10, "Connection timeout in seconds")

	// Execute
	if err := rootCmd.Execute(); err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

```

--------------------------------------------------------------------------------
/examples/airtabl3/index.ts:
--------------------------------------------------------------------------------

```typescript
import { create } from "@a0dotrun/expose"
import { tool } from "@a0dotrun/expose/tool"
import { z } from "zod"
import { createRecord, fetchRecords, updateRecord } from "./lib/index"

const StageSchema = z.enum(["Prospect", "Qualified", "Closed"])

const createCRMRecord = tool({
  name: "create_sales_crm_record",
  description: "Creates a new record in Sales CRM and generates a unique ID",
  args: z.object({
    email: z.string().email(),
    company: z.string(),
    stage: StageSchema,
    contact: z.string(),
    notes: z.string(),
  }),
  async run(args) {
    const { data, error } = await createRecord({
      Email: args.email,
      Company: args.company,
      Stage: args.stage,
      Contact: args.contact,
      Notes: args.notes,
    })
    if (error) {
      throw new Error(error.message)
    }
    return data
  },
})

const listCRMRecords = tool({
  name: "list_sales_crm_records",
  description: "Lists all sales CRM records",
  async run() {
    const { data, error } = await fetchRecords()
    if (error) {
      throw new Error(error.message)
    }
    return data
  },
})

const updateCRMRecord = tool({
  name: "update_sales_crm_record",
  description: "Updates an existing record in Sales CRM",
  args: z.object({
    id: z.string(),
    email: z.string().email().optional(),
    company: z.string().optional(),
    stage: StageSchema.optional(),
    contact: z.string().optional(),
    notes: z.string().optional(),
  }),
  async run(args) {
    const { id, ...others } = args
    const { data, error } = await updateRecord(id, {
      Email: others.email,
      Company: others.company,
      Stage: others.stage,
      Contact: others.contact,
      Notes: others.notes,
    })
    if (error) {
      throw new Error(error.message)
    }
    return data
  },
})

const app = create({
  tools: [createCRMRecord, listCRMRecords, updateCRMRecord],
})

export default {
  port: 3000,
  fetch: app.fetch,
}

```

--------------------------------------------------------------------------------
/examples/airtabl3/lib/index.ts:
--------------------------------------------------------------------------------

```typescript
import Airtable from "airtable"

const base = new Airtable({
  apiKey: `...`,
}).base("...")

interface NewRecord {
  Email: string
  Company: string
  Stage: string
  Contact: string
  Notes: string
}

interface UpdateRecord {
  Email?: string
  Company?: string
  Stage?: string
  Contact?: string
  Notes?: string
}

export async function fetchRecords() {
  try {
    const records = await base("Default").select({ view: "Grid view" }).all()
    const results = records.map((record) => ({
      id: record.id,
      company: record.fields.Company,
      stage: record.fields.Stage,
      contact: record.fields.Contact,
      notes: record.fields.Notes,
      email: record.fields.Email,
    }))
    return {
      data: results,
      error: null,
    }
  } catch (err) {
    console.error(err)
    return {
      data: null,
      error: err,
    }
  }
}

export async function createRecord(args: NewRecord) {
  try {
    const record = await base("Default").create({
      Email: args.Email,
      Company: args.Company,
      Stage: args.Stage,
      Contact: args.Contact,
      Notes: args.Notes,
    })
    return {
      data: record.id,
      error: null,
    }
  } catch (err) {
    console.error(err)
    return {
      data: null,
      error: err,
    }
  }
}

export async function updateRecord(id: string, args: UpdateRecord) {
  try {
    const record = await base("Default").update(id, {
      Email: args.Email || undefined,
      Company: args.Company || undefined,
      Stage: args.Stage || undefined,
      Contact: args.Contact || undefined,
      Notes: args.Notes || undefined,
    })
    return {
      data: record.id,
      error: null,
    }
  } catch (err) {
    console.error(err)
    return {
      data: null,
      error: err,
    }
  }
}

// createRecord({
//   Email: "[email protected]",
//   Company: "Aravind Srinivasan",
//   Stage: "Prospect",
//   Contact: "Aravind Srinivasan",
//   Notes: "AI search engine",
// })

// ;(async () => {
//   const results = await fetchRecords()
//   console.log(results)
// })()

// updateRecord("rec9FCaEJEcis2fub", { Contact: "Amit Singh" })

```

--------------------------------------------------------------------------------
/download_cli.sh:
--------------------------------------------------------------------------------

```bash
#!/usr/bin/env bash
set -eu

##############################################################################
# Expose CLI Install Script
#
# This script downloads the latest stable 'expose-cli' binary from GitHub releases
# and installs it to your system.
#
# Supported OS: macOS (darwin), Linux
# Supported Architectures: x86_64, arm64
#
# Usage:
#   curl -fsSL https://github.com/a0dotrun/expose/releases/download/stable/install.sh | bash
#
##############################################################################

# --- 1) Check for curl ---
if ! command -v curl >/dev/null 2>&1; then
  echo "Error: 'curl' is required to download Expose CLI. Please install curl and try again."
  exit 1
fi

# --- 2) Detect OS/Architecture ---
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
ARCH=$(uname -m)

case "$OS" in
  linux) OS_ID="unknown-linux-gnu" ;;
  darwin) OS_ID="apple-darwin" ;;
  *)
    echo "Error: Unsupported OS '$OS'. Expose CLI only supports Linux and macOS."
    exit 1
    ;;
esac

case "$ARCH" in
  x86_64|amd64)
    ARCH_ID="x86_64"
    ;;
  arm64|aarch64)
    ARCH_ID="aarch64"
    ;;
  *)
    echo "Error: Unsupported architecture '$ARCH'. Expose CLI supports x86_64 and arm64."
    exit 1
    ;;
esac

# --- 3) Set download URL ---
REPO="a0dotrun/expose"
INSTALL_DIR="${EXPOSE_BIN_DIR:-"$HOME/.local/bin"}"
FILE="expose-cli-${ARCH_ID}-${OS_ID}.tar.bz2"
DOWNLOAD_URL="https://github.com/$REPO/releases/download/stable/$FILE"

# --- 4) Download the binary ---
echo "Downloading $FILE from $DOWNLOAD_URL..."
curl -sLf "$DOWNLOAD_URL" --output "$FILE"

# --- 5) Extract & Install ---
mkdir -p "$INSTALL_DIR"
tar -xjf "$FILE" -C "$INSTALL_DIR"
rm "$FILE"

# Ensure it's executable
chmod +x "$INSTALL_DIR/expose-cli"

echo "✅ Expose CLI installed successfully at $INSTALL_DIR/expose-cli"

# --- 6) Add to PATH if needed ---
if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
  echo ""
  echo "Warning: Expose CLI installed, but $INSTALL_DIR is not in your PATH."
  echo "Add it by running:"
  echo "    export PATH=\"$INSTALL_DIR:\$PATH\""
  echo "Then reload your shell (e.g. 'source ~/.bashrc', 'source ~/.zshrc')."
fi

echo "Run 'expose-cli --help' to get started!"

```

--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------

```yaml
# This workflow is main release, needs to be manually tagged & pushed.
on:
  push:
    paths-ignore:
      - "docs/**"
    tags:
      - "v*"

name: Release
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  # ------------------------------------
  # 1) Build CLI for multiple OS/Arch (Linux, MacOS)
  # ------------------------------------
  build-cli:
    uses: ./.github/workflows/build-cli.yml

  # ------------------------------------
  # 2) Upload Install CLI Script
  # ------------------------------------
  install-script:
    name: Upload Install Script
    runs-on: ubuntu-latest
    needs: [build-cli]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/upload-artifact@v4
        with:
          name: download_cli.sh
          path: download_cli.sh

  # ------------------------------------
  # 3) Create/Update GitHub Release CLI
  # ------------------------------------
  release-cli:
    name: Release CLI
    runs-on: ubuntu-latest
    needs: [build-cli, install-script]
    permissions:
      contents: write
    steps:
      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          merge-multiple: true

      - name: Release versioned
        uses: ncipollo/release-action@v1
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          artifacts: |
            expose-cli-*.tar.bz2
            download_cli.sh
          allowUpdates: true
          omitBody: true
          omitPrereleaseDuringUpdate: true

      - name: Release CLI Stable
        uses: ncipollo/release-action@v1
        with:
          tag: stable
          name: Stable
          token: ${{ secrets.GITHUB_TOKEN }}
          artifacts: |
            expose-cli-*.tar.bz2
            download_cli.sh
          allowUpdates: true
          omitBody: true
          omitPrereleaseDuringUpdate: true
  # ------------------------------------
  # 4) Publish NPM Package
  # ------------------------------------
  publish-npm:
    name: Publish NPM Package
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3

      - name: Setup Bun
        uses: oven-sh/setup-bun@v2

      - name: Install dependencies
        run: bun install

      - name: Authenticate NPM
        run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc

      - name: Run publish
        run: make publish
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

```

--------------------------------------------------------------------------------
/.github/workflows/build-cli.yml:
--------------------------------------------------------------------------------

```yaml
# This is a **reuseable** workflow that bundles the CLI
# It doesn't get triggered on its own. It gets used in multiple workflows:
#  - release.yml
on:
  workflow_call:
    inputs:
      version:
        required: false
        default: ""
        type: string
      # Let's allow overriding the OSes and architectures in JSON array form:
      # e.g. '["ubuntu-latest","macos-latest"]'
      # If no input is provided, these defaults apply.
      operating-systems:
        type: string
        required: false
        default: '["ubuntu-latest","macos-latest"]'
      architectures:
        type: string
        required: false
        default: '["x86_64","aarch64"]'

name: "Reusable workflow to build CLI"

jobs:
  build-cli:
    name: Build CLI
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: ${{ fromJson(inputs.operating-systems) }}
        architecture: ${{ fromJson(inputs.architectures) }}
        include:
          - os: ubuntu-latest
            target-suffix: unknown-linux-gnu
            goos: linux
            os_name: linux
          - os: macos-latest
            target-suffix: apple-darwin
            goos: darwin
            os_name: macos

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: stable

      - name: Install jq
        run: |
          if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
            sudo apt-get update && sudo apt-get install -y jq
          elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then
            brew install jq
          fi

      - name: Extract version
        run: |
          VERSION=$(jq -r '.version' packages/expose/package.json)
          echo "VERSION=$VERSION" >> $GITHUB_ENV
          echo "Extracted version: $VERSION"

      - name: Set GOARCH
        run: |
          if [[ "${{ matrix.architecture }}" == "x86_64" ]]; then
            echo "GOARCH=amd64" >> $GITHUB_ENV
          elif [[ "${{ matrix.architecture }}" == "aarch64" ]]; then
            echo "GOARCH=arm64" >> $GITHUB_ENV
          fi

      - name: Build binary
        env:
          GOOS: ${{ matrix.goos }}
          GOARCH: ${{ env.GOARCH }}
        run: |
          TARGET="${{ matrix.architecture }}-${{ matrix.target-suffix }}"
          echo "Building for target: ${TARGET} (GOOS=$GOOS, GOARCH=$GOARCH)"

          # Create output directory
          mkdir -p build/${TARGET}/release

          # Build the binary
          go build -o build/${TARGET}/release/expose-cli cmd/cli.go

          # Create tarball
          cd build/${TARGET}/release
          tar -cjf expose-cli-${TARGET}.tar.bz2 expose-cli
          cd -

          # Set artifact path for upload step
          echo "ARTIFACT=build/${TARGET}/release/expose-cli-${TARGET}.tar.bz2" >> $GITHUB_ENV

          echo "Build complete: ${{ env.ARTIFACT }}"

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: expose-cli-${{ matrix.architecture }}-${{ matrix.target-suffix }}
          path: ${{ env.ARTIFACT }}

```

--------------------------------------------------------------------------------
/packages/expose/src/mcp.ts:
--------------------------------------------------------------------------------

```typescript
import {
  CallToolRequestSchema,
  CallToolResult,
  ErrorCode,
  InitializeRequestSchema,
  InitializeResult,
  JSONRPCError,
  JSONRPCRequest,
  JSONRPCResponse,
  ListToolsRequestSchema,
  ListToolsResult,
} from "@modelcontextprotocol/sdk/types.js"
import { z } from "zod"
import { zodToJsonSchema } from "zod-to-json-schema"
import { Tool } from "./tool.js"

class MCPError extends Error {
  constructor(
    message: string,
    public code: ErrorCode,
  ) {
    super(message)
  }
}

const RequestSchema = z.union([
  InitializeRequestSchema,
  ListToolsRequestSchema,
  CallToolRequestSchema,
])

type RequestSchema = z.infer<typeof RequestSchema>

export function createMcp(input: { tools: Tool[] }) {
  return {
    async process(message: JSONRPCRequest) {
      try {
        const parsed = RequestSchema.parse(message)
        return await (async (): Promise<JSONRPCResponse> => {
          if (parsed.method === "initialize")
            return {
              jsonrpc: "2.0",
              id: message.id,
              result: {
                protocolVersion: "2024-11-05",
                capabilities: {
                  tools: {},
                },
                serverInfo: {
                  name: "expose",
                  version: "0.0.1",
                },
              } as InitializeResult,
            }

          if (parsed.method === "tools/list") {
            return {
              jsonrpc: "2.0",
              id: message.id,
              result: {
                tools: input.tools.map((tool) => ({
                  name: tool.name,
                  inputSchema: tool.args
                    ? (zodToJsonSchema(tool.args as any, "args").definitions![
                        "args"
                      ] as any)
                    : { type: "object" },
                  description: tool.description,
                })),
              } as ListToolsResult,
            } satisfies JSONRPCResponse
          }

          if (parsed.method === "tools/call") {
            const tool = input.tools.find(
              (tool) => tool.name === parsed.params.name,
            )
            if (!tool)
              throw new MCPError("Tool not found", ErrorCode.MethodNotFound)

            let args = parsed.params.arguments
            if (tool.args) {
              const validated = await tool.args["~standard"].validate(args)
              if (validated.issues)
                throw new MCPError("Invalid arguments", ErrorCode.InvalidParams)
              args = validated.value as any
            }

            return tool
              .run(args)
              .catch(
                (error) =>
                  ({
                    jsonrpc: "2.0",
                    id: message.id,
                    error: {
                      code: ErrorCode.InternalError,
                      message: error.message,
                    },
                  }) satisfies JSONRPCError,
              )
              .then(
                (result) =>
                  ({
                    jsonrpc: "2.0",
                    id: message.id,
                    result: {
                      content: [
                        {
                          type: "text",
                          text: JSON.stringify(result, null, 2),
                        },
                      ],
                    } as CallToolResult,
                  }) satisfies JSONRPCResponse,
              )
          }
          throw new MCPError("Method not found", ErrorCode.MethodNotFound)
        })()
      } catch (error) {
        if (error instanceof MCPError) {
          const code = error.code
          return {
            jsonrpc: "2.0",
            id: message.id,
            error: { code, message: error.message },
          } satisfies JSONRPCError
        }
        return {
          jsonrpc: "2.0",
          id: message.id,
          error: {
            code: ErrorCode.InternalError,
            message: "Internal error",
          },
        } satisfies JSONRPCError
      }
    },
  }
}

```

--------------------------------------------------------------------------------
/expose.go:
--------------------------------------------------------------------------------

```go
package expose

import (
	"bufio"
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/mark3labs/mcp-go/mcp"
)

type Client struct {
	baseURL    string
	httpClient *http.Client
	headers    map[string]string
}

func NewClient(baseURL string, timeout time.Duration) *Client {
	return &Client{
		baseURL: baseURL,
		httpClient: &http.Client{
			Timeout: timeout,
		},
		headers: map[string]string{
			"Content-Type": "application/json",
		},
	}
}

func (c *Client) SetHeader(key, value string) {
	c.headers[key] = value
}

func (c *Client) MakePostRequest(
	ctx context.Context,
	endpoint string,
	body interface{},
) ([]byte, error) {
	url := fmt.Sprintf("%s%s", c.baseURL, endpoint)
	var requestBody []byte
	var err error
	if body != nil {
		requestBody, err = json.Marshal(body)
		if err != nil {
			return nil, fmt.Errorf("failed to marshal request body: %w", err)
		}
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(requestBody))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	for key, value := range c.headers {
		req.Header.Set(key, value)
	}

	resp, err := c.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}
	defer resp.Body.Close()

	responseBody, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, fmt.Errorf("failed to read response body: %w", err)
	}

	return responseBody, nil
}

func createErrorResponse(
	id interface{},
	code int,
	message string,
) mcp.JSONRPCMessage {
	return mcp.JSONRPCError{
		JSONRPC: mcp.JSONRPC_VERSION,
		ID:      id,
		Error: struct {
			Code    int         `json:"code"`
			Message string      `json:"message"`
			Data    interface{} `json:"data,omitempty"`
		}{
			Code:    code,
			Message: message,
		},
	}
}

func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage {
	return mcp.JSONRPCResponse{
		JSONRPC: mcp.JSONRPC_VERSION,
		ID:      id,
		Result:  result,
	}
}

type ProxyStdioServer struct {
	BaseURL   string
	Client    *Client
	errLogger *log.Logger
}

func NewProxyStdioServer(baseURL string, timeout time.Duration) *ProxyStdioServer {
	return &ProxyStdioServer{
		BaseURL:   baseURL,
		Client:    NewClient(baseURL, timeout),
		errLogger: log.New(os.Stderr, "", log.LstdFlags),
	}
}

func (s *ProxyStdioServer) SetBaseURL(baseURL string) {
	s.BaseURL = baseURL
}

func (s *ProxyStdioServer) SetErrLogger(errLogger *log.Logger) {
	s.errLogger = errLogger
}

func (s *ProxyStdioServer) Listen(
	ctx context.Context,
	stdin io.Reader,
	stdout io.Writer,
) error {

	reader := bufio.NewReader(stdin)

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
			// Use a goroutine to make the read cancellable
			readChan := make(chan string, 1)
			errChan := make(chan error, 1)

			go func() {
				line, err := reader.ReadString('\n')
				if err != nil {
					errChan <- err
					return
				}
				readChan <- line
			}()

			select {
			case <-ctx.Done():
				return ctx.Err()
			case err := <-errChan:
				if err == io.EOF {
					return nil
				}
				s.errLogger.Printf("Error reading input: %v", err)
				return err
			case line := <-readChan:
				if err := s.proxyMessage(ctx, line, stdout); err != nil {
					if err == io.EOF {
						return nil
					}
					s.errLogger.Printf("Error handling message: %v", err)
					return err
				}
			}
		}
	}
}

func (s *ProxyStdioServer) handleMessage(
	ctx context.Context,
	message json.RawMessage,
) mcp.JSONRPCMessage {
	var baseMessage struct {
		JSONRPC string      `json:"jsonrpc"`
		Method  string      `json:"method"`
		ID      interface{} `json:"id,omitempty"`
	}

	if err := json.Unmarshal(message, &baseMessage); err != nil {
		return createErrorResponse(nil, mcp.PARSE_ERROR, "Failed to parse message")
	}

	// Check for valid JSONRPC version
	if baseMessage.JSONRPC != mcp.JSONRPC_VERSION {
		return createErrorResponse(
			baseMessage.ID,
			mcp.INVALID_REQUEST,
			"Invalid JSON-RPC version",
		)
	}

	if baseMessage.ID == nil {
		return nil
	}

	if baseMessage.Method == "ping" {
		var request mcp.PingRequest
		if err := json.Unmarshal(message, &request); err != nil {
			return createErrorResponse(
				baseMessage.ID,
				mcp.INVALID_REQUEST,
				"Invalid ping request",
			)
		}
		return createResponse(baseMessage.ID, mcp.EmptyResult{})
	}

	switch baseMessage.Method {
	case "initialize", "tools/list", "tools/call":
		response, err := s.Client.MakePostRequest(ctx, "/", message)
		if err != nil {
			return createErrorResponse(
				baseMessage.ID,
				mcp.INTERNAL_ERROR,
				"Failed to make request",
			)
		}
		var responseMessage mcp.JSONRPCMessage
		if err := json.Unmarshal(response, &responseMessage); err != nil {
			return createErrorResponse(
				baseMessage.ID,
				mcp.INTERNAL_ERROR,
				"Failed to parse response",
			)
		}
		return responseMessage
	default:
		return createErrorResponse(
			baseMessage.ID,
			mcp.METHOD_NOT_FOUND,
			fmt.Sprintf("Method %s not supported", baseMessage.Method),
		)
	}
}

func (s *ProxyStdioServer) proxyMessage(
	ctx context.Context,
	line string,
	writer io.Writer,
) error {
	var rawMessage json.RawMessage
	if err := json.Unmarshal([]byte(line), &rawMessage); err != nil {
		response := createErrorResponse(nil, mcp.PARSE_ERROR, "Parse error")
		return s.writeResponse(response, writer)
	}

	response := s.handleMessage(ctx, rawMessage)
	if response != nil {
		return s.writeResponse(response, writer)
	}
	return nil
}

// writeResponse marshals and writes a JSON-RPC response message followed by a newline.
// Returns an error if marshaling or writing fails.
func (s *ProxyStdioServer) writeResponse(
	response mcp.JSONRPCMessage,
	writer io.Writer,
) error {
	responseBytes, err := json.Marshal(response)
	if err != nil {
		return err
	}

	// Write response followed by newline
	if _, err := fmt.Fprintf(writer, "%s\n", responseBytes); err != nil {
		return err
	}

	return nil
}

func ProxyServeStdio(baseURL string, timeout time.Duration) error {
	ps := NewProxyStdioServer(baseURL, timeout)
	ps.SetErrLogger(log.New(os.Stderr, "", log.LstdFlags))

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)

	go func() {
		<-sigChan
		cancel()
	}()

	return ps.Listen(ctx, os.Stdin, os.Stdout)
}

```