#
tokens: 13097/50000 30/30 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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:
--------------------------------------------------------------------------------

```
 1 | # dependencies (bun install)
 2 | node_modules
 3 | 
 4 | # output
 5 | out
 6 | dist
 7 | *.tgz
 8 | 
 9 | # code coverage
10 | coverage
11 | *.lcov
12 | 
13 | # logs
14 | logs
15 | _.log
16 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17 | 
18 | # dotenv environment variable files
19 | .env
20 | .env.development.local
21 | .env.test.local
22 | .env.production.local
23 | .env.local
24 | 
25 | # caches
26 | .eslintcache
27 | .cache
28 | *.tsbuildinfo
29 | 
30 | # IntelliJ based IDEs
31 | .idea
32 | 
33 | # Finder (MacOS) folder config
34 | .DS_Store
35 | 
36 | tmp/
37 | 
```

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

```markdown
1 | # Changesets
2 | 
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 | 
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 | 
```

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

```markdown
  1 | ---
  2 | title: Expose
  3 | description: Easily build MCP tools for Claude desktop app
  4 | ---
  5 | 
  6 | Expose lets you build MCP tools that you can invoke with MCP client like Claude desktop app.
  7 | 
  8 | - **Self-hosted**: You can easily self-host tools and deploy them on your own server.
  9 | - **Unified gateway**: Generates a single HTTP endpoint that you can register with `expose-cli`
 10 | - **Flexible**: Easily configure and customize your tools to fit your needs.
 11 | 
 12 | ## Getting started
 13 | 
 14 | 1. **Setup expose CLI**
 15 | 
 16 |    ```bash
 17 |    curl -fsSL https://github.com/a0dotrun/expose/releases/download/stable/download_cli.sh | bash
 18 |    ```
 19 | 
 20 | 2. **Install dependencies**
 21 | 
 22 |    ```bash
 23 |    npm i @a0dotrun/expose
 24 |    ```
 25 | 
 26 | 3. **Create server**
 27 | 
 28 |    ```bash
 29 |    touch src/server.ts
 30 |    ```
 31 | 
 32 |    ```ts title=src/server.ts
 33 |    import { create } from "@a0dotrun/expose"
 34 | 
 35 |    const app = create({
 36 |      tools: [],
 37 |    })
 38 | 
 39 |    export default {
 40 |      port: 3000,
 41 |      fetch: app.fetch,
 42 |    }
 43 |    ```
 44 | 
 45 | 4. **Define your tools**
 46 | 
 47 | ```diff lang=ts title=src/server.ts
 48 | + import { tool } from "@a0dotrun/expose/tool"
 49 | + import { subscription } from "@acme/lib/subscription"
 50 | + import { z } from "zod"
 51 | 
 52 | + const getCustomerSubscription = tool({
 53 | +   name: "getCustomerSubscription",
 54 | +   description: "Get subscription information for a customer",
 55 | +   args: z.object({
 56 | +       customer: z.string().uuid()
 57 | +   }),
 58 | +   run: async (input) => {
 59 | +     // Your subscription logic here
 60 | +     return input;
 61 | +   },
 62 | + });
 63 | 
 64 | 
 65 | + const createCustomerSubscription = tool({
 66 | +   name: "createCustomerSubscription",
 67 | +   description: "Create a subscription for a customer",
 68 | +   args: z.object({
 69 | +       customer: z.string().uuid()
 70 | +   }),
 71 | +   run: async (input) => {
 72 | +     // Your subscription logic here
 73 | +     return input;
 74 | +   },
 75 | + });
 76 | 
 77 | const app = create({
 78 |   tools: [
 79 | +    getCustomerSubscription,
 80 | +    createCustomerSubscription,
 81 |   ],
 82 | });
 83 | ```
 84 | 
 85 | 5. **Start server**
 86 | 
 87 |    ```bash
 88 |    npm run dev
 89 |    ```
 90 | 
 91 |    You can also deploy the server and note down the public URL.
 92 | 
 93 | 6. **Register in Claude desktop app**
 94 |    MACOS Claude desktop MCP config path: `~/Library/Application Support/Claude/claude_desktop_config.json`
 95 | 
 96 | ```json
 97 | {
 98 |   ...
 99 |   "mcpServers": {
100 |     "subscriptionManager": {
101 |       "command": "/Users/acmeuser/.local/bin/expose-cli",
102 |       "args": ["--url", "http://localhost:3000", "--timeout", "15"]
103 |     }
104 |   }
105 |   ...
106 | }
107 | ```
108 | 
109 | _replace localhost with your public URL_
110 | 
111 | ---
112 | 
113 | expose is created by [@\_sanchitrk](https://x.com/_sanchitrk) at [a0](https://a0.run)
114 | 
115 | ### Acknowledgements
116 | 
117 | I was inspired by [opencontrol](https://github.com/toolbeam/opencontrol)
118 | 
```

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

```toml
1 | [install]
2 | exact = true
```

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

```markdown
1 | ---
2 | "@a0dotrun/expose": patch
3 | ---
4 | 
5 | tweaks
6 | 
```

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

```markdown
1 | ---
2 | "@a0dotrun/expose": patch
3 | ---
4 | 
5 | updated install script
6 | 
```

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

```markdown
1 | ---
2 | "@a0dotrun/expose": patch
3 | ---
4 | 
5 | fixes install expose-cli script
6 | 
```

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

```json
1 | {
2 |   "name": "@expose/example-echo",
3 |   "private": true,
4 |   "version": "0.0.0",
5 |   "dependencies": {
6 |     "@a0dotrun/expose": "workspace:*"
7 |   }
8 | }
9 | 
```

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

```json
1 | {
2 |   "name": "@expose/example-systeminfo",
3 |   "private": true,
4 |   "version": "0.0.0",
5 |   "dependencies": {
6 |     "@a0dotrun/expose": "workspace:*"
7 |   }
8 | }
9 | 
```

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

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

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

```json
 1 | {
 2 |   "name": "@expose/example-airtabl3",
 3 |   "private": true,
 4 |   "version": "0.0.0",
 5 |   "dependencies": {
 6 |     "@a0dotrun/expose": "workspace:*",
 7 |     "airtable": "0.12.2"
 8 |   }
 9 | }
10 | 
```

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

```json
1 | {
2 |   "$schema": "https://json.schemastore.org/tsconfig",
3 |   "extends": "@tsconfig/node22/tsconfig.json",
4 |   "compilerOptions": {
5 |     "outDir": "dist",
6 |     "declaration": true
7 |   }
8 | }
9 | 
```

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

```markdown
 1 | # expose
 2 | 
 3 | ## 0.0.4
 4 | 
 5 | ### Patch Changes
 6 | 
 7 | - 3a6f233: gh tweaks
 8 | 
 9 | ## 0.0.3
10 | 
11 | ### Patch Changes
12 | 
13 | - a8abced: fix package naming
14 | 
15 | ## 0.0.2
16 | 
17 | ### Patch Changes
18 | 
19 | - 35fe442: v0 working example with examples
20 | 
```

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

```json
 1 | {
 2 |   "$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
 3 |   "changelog": "@changesets/cli/changelog",
 4 |   "commit": "./commit.cjs",
 5 |   "fixed": [["@a0dotrun/expose"]],
 6 |   "linked": [],
 7 |   "access": "public",
 8 |   "baseBranch": "main",
 9 |   "updateInternalDependencies": "patch",
10 |   "ignore": ["@expose/example-*"]
11 | }
12 | 
```

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

```json
 1 | {
 2 |   "name": "expose",
 3 |   "private": true,
 4 |   "type": "module",
 5 |   "packageManager": "bun",
 6 |   "workspaces": [
 7 |     "packages/*",
 8 |     "examples/*"
 9 |   ],
10 |   "devDependencies": {
11 |     "@changesets/cli": "2.28.1",
12 |     "@tsconfig/node22": "22.0.0",
13 |     "@types/node": "22.13.9",
14 |     "prettier": "3.5.3",
15 |     "typescript": "^5"
16 |   },
17 |   "prettier": {
18 |     "semi": false
19 |   }
20 | }
21 | 
```

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

```typescript
 1 | import os from "os"
 2 | 
 3 | export function serverInfo() {
 4 |   return {
 5 |     hostname: os.hostname(),
 6 |     platform: os.platform(),
 7 |     arch: os.arch(),
 8 |     uptime: os.uptime(),
 9 |     totalMemory: os.totalmem(),
10 |     freeMemory: os.freemem(),
11 |     cpus: os.cpus().map((cpu) => ({
12 |       model: cpu.model,
13 |       speed: cpu.speed,
14 |     })),
15 |     networkInterfaces: os.networkInterfaces(),
16 |     loadAvg: os.loadavg(),
17 |   }
18 | }
19 | 
```

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

```typescript
 1 | import { create } from "@a0dotrun/expose"
 2 | import { tool } from "@a0dotrun/expose/tool"
 3 | import { z } from "zod"
 4 | 
 5 | const echo = tool({
 6 |   name: "echo",
 7 |   description: "echoes the message back",
 8 |   args: z.object({
 9 |     message: z.string(),
10 |   }),
11 |   async run(args) {
12 |     return "Echo from server: " + args.message
13 |   },
14 | })
15 | 
16 | const app = create({
17 |   tools: [echo],
18 | })
19 | 
20 | export default {
21 |   port: 3000,
22 |   fetch: app.fetch,
23 | }
24 | 
```

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

```typescript
 1 | import { create } from "@a0dotrun/expose"
 2 | import { tool } from "@a0dotrun/expose/tool"
 3 | import { serverInfo } from "./lib/info"
 4 | 
 5 | const systemInfo = tool({
 6 |   name: "systemInfo",
 7 |   description:
 8 |     "Get sytem information about the server like uptime, memory, cpu, etc.",
 9 |   run: async () => {
10 |     return serverInfo()
11 |   },
12 | })
13 | 
14 | const app = create({
15 |   tools: [systemInfo],
16 | })
17 | 
18 | export default {
19 |   port: 3000,
20 |   fetch: app.fetch,
21 | }
22 | 
```

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

```yaml
 1 | name: Format and Lint
 2 | 
 3 | on:
 4 |   push:
 5 |     branches: [main]
 6 |   pull_request:
 7 |   workflow_dispatch:
 8 | 
 9 | jobs:
10 |   format:
11 |     runs-on: ubuntu-latest
12 | 
13 |     permissions:
14 |       contents: write
15 |       pull-requests: write
16 | 
17 |     steps:
18 |       - uses: actions/checkout@v4
19 |         with:
20 |           ref: ${{ github.head_ref }}
21 |           fetch-depth: 0
22 |       - uses: oven-sh/setup-bun@v2
23 |       - run: |
24 |           git config --local user.email "[email protected]"
25 |           git config --local user.name "gh-actions-bot"
26 |           make format
27 | 
```

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

```json
 1 | {
 2 |   "$schema": "https://json.schemastore.org/package.json",
 3 |   "name": "@a0dotrun/expose",
 4 |   "version": "0.0.4",
 5 |   "type": "module",
 6 |   "scripts": {
 7 |     "build": "bun tsc"
 8 |   },
 9 |   "exports": {
10 |     ".": "./dist/index.js",
11 |     "./*": "./dist/*.js"
12 |   },
13 |   "dependencies": {
14 |     "@modelcontextprotocol/sdk": "1.6.1",
15 |     "@tsconfig/bun": "1.0.7",
16 |     "hono": "4.7.4",
17 |     "zod": "3.24.2",
18 |     "zod-to-json-schema": "3.24.3"
19 |   },
20 |   "devDependencies": {
21 |     "@standard-schema/spec": "1.0.0"
22 |   },
23 |   "publishConfig": {
24 |     "access": "public"
25 |   }
26 | }
27 | 
```

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

```typescript
 1 | import { StandardSchemaV1 } from "@standard-schema/spec"
 2 | import { z } from "zod"
 3 | 
 4 | export interface Tool<
 5 |   Args extends undefined | StandardSchemaV1 = undefined | StandardSchemaV1,
 6 | > {
 7 |   name: string
 8 |   description: string
 9 |   args?: Args
10 |   run: Args extends StandardSchemaV1
11 |     ? (args: StandardSchemaV1.InferOutput<Args>) => Promise<any>
12 |     : () => Promise<any>
13 | }
14 | 
15 | export function tool<Args extends undefined | StandardSchemaV1>(
16 |   input: Tool<Args>,
17 | ) {
18 |   return input
19 | }
20 | 
21 | // Example
22 | 
23 | tool({
24 |   name: "echo",
25 |   description: "echoes the message back",
26 |   args: z.object({
27 |     message: z.string(),
28 |   }),
29 |   async run(args) {
30 |     return args.message
31 |   },
32 | })
33 | 
```

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

```typescript
 1 | import { Hono } from "hono"
 2 | // import { HTTPException } from "hono/http-exception"
 3 | import { Tool } from "./tool.js"
 4 | import { createMcp } from "./mcp.js"
 5 | 
 6 | export interface ExposeOptions {
 7 |   token?: string
 8 | }
 9 | 
10 | export function create(input: { tools: Tool[]; key?: string }) {
11 |   const mcp = createMcp({ tools: input.tools })
12 | 
13 |   return (
14 |     new Hono()
15 |       // .use((c, next) => {
16 |       //   if (input?.key) {
17 |       //     const authorization = c.req.header("Authorization")
18 |       //     if (authorization !== `Bearer ${input?.key}`) {
19 |       //       throw new HTTPException(401)
20 |       //     }
21 |       //   }
22 |       //   return next()
23 |       // })
24 |       .post("/", async (c) => {
25 |         const body = await c.req.json()
26 |         console.log("mcp request", body)
27 |         const result = await mcp.process(body)
28 |         console.log("mcp response", result)
29 |         return c.json(result)
30 |       })
31 |   )
32 | }
33 | 
```

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

```go
 1 | package main
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"log"
 6 | 	"os"
 7 | 	"time"
 8 | 
 9 | 	"github.com/a0dotrun/expose"
10 | 	"github.com/spf13/cobra"
11 | )
12 | 
13 | func main() {
14 | 	var url string
15 | 	var timeout int
16 | 
17 | 	rootCmd := &cobra.Command{
18 | 		Use:   "expose-cli",
19 | 		Short: "A CLI tool for proxying MCP tools",
20 | 		Long:  `expose-cli is a command-line tool that proxies MCP tools to a specified URL with configurations`,
21 | 		Run: func(cmd *cobra.Command, args []string) {
22 | 			if url == "" {
23 | 				fmt.Println("Error: URL is required")
24 | 				cmd.Help()
25 | 				os.Exit(1)
26 | 			}
27 | 
28 | 			timeoutDuration := time.Duration(timeout) * time.Second
29 | 			if err := expose.ProxyServeStdio(url, timeoutDuration); err != nil {
30 | 				log.Fatalf("Failed to start proxy server: %v", err)
31 | 			}
32 | 		},
33 | 	}
34 | 
35 | 	// Define flags
36 | 	rootCmd.Flags().StringVar(&url, "url", "", "Target URL to proxy (required)")
37 | 	rootCmd.Flags().IntVar(&timeout, "timeout", 10, "Connection timeout in seconds")
38 | 
39 | 	// Execute
40 | 	if err := rootCmd.Execute(); err != nil {
41 | 		fmt.Println(err)
42 | 		os.Exit(1)
43 | 	}
44 | }
45 | 
```

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

```typescript
 1 | import { create } from "@a0dotrun/expose"
 2 | import { tool } from "@a0dotrun/expose/tool"
 3 | import { z } from "zod"
 4 | import { createRecord, fetchRecords, updateRecord } from "./lib/index"
 5 | 
 6 | const StageSchema = z.enum(["Prospect", "Qualified", "Closed"])
 7 | 
 8 | const createCRMRecord = tool({
 9 |   name: "create_sales_crm_record",
10 |   description: "Creates a new record in Sales CRM and generates a unique ID",
11 |   args: z.object({
12 |     email: z.string().email(),
13 |     company: z.string(),
14 |     stage: StageSchema,
15 |     contact: z.string(),
16 |     notes: z.string(),
17 |   }),
18 |   async run(args) {
19 |     const { data, error } = await createRecord({
20 |       Email: args.email,
21 |       Company: args.company,
22 |       Stage: args.stage,
23 |       Contact: args.contact,
24 |       Notes: args.notes,
25 |     })
26 |     if (error) {
27 |       throw new Error(error.message)
28 |     }
29 |     return data
30 |   },
31 | })
32 | 
33 | const listCRMRecords = tool({
34 |   name: "list_sales_crm_records",
35 |   description: "Lists all sales CRM records",
36 |   async run() {
37 |     const { data, error } = await fetchRecords()
38 |     if (error) {
39 |       throw new Error(error.message)
40 |     }
41 |     return data
42 |   },
43 | })
44 | 
45 | const updateCRMRecord = tool({
46 |   name: "update_sales_crm_record",
47 |   description: "Updates an existing record in Sales CRM",
48 |   args: z.object({
49 |     id: z.string(),
50 |     email: z.string().email().optional(),
51 |     company: z.string().optional(),
52 |     stage: StageSchema.optional(),
53 |     contact: z.string().optional(),
54 |     notes: z.string().optional(),
55 |   }),
56 |   async run(args) {
57 |     const { id, ...others } = args
58 |     const { data, error } = await updateRecord(id, {
59 |       Email: others.email,
60 |       Company: others.company,
61 |       Stage: others.stage,
62 |       Contact: others.contact,
63 |       Notes: others.notes,
64 |     })
65 |     if (error) {
66 |       throw new Error(error.message)
67 |     }
68 |     return data
69 |   },
70 | })
71 | 
72 | const app = create({
73 |   tools: [createCRMRecord, listCRMRecords, updateCRMRecord],
74 | })
75 | 
76 | export default {
77 |   port: 3000,
78 |   fetch: app.fetch,
79 | }
80 | 
```

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

```typescript
  1 | import Airtable from "airtable"
  2 | 
  3 | const base = new Airtable({
  4 |   apiKey: `...`,
  5 | }).base("...")
  6 | 
  7 | interface NewRecord {
  8 |   Email: string
  9 |   Company: string
 10 |   Stage: string
 11 |   Contact: string
 12 |   Notes: string
 13 | }
 14 | 
 15 | interface UpdateRecord {
 16 |   Email?: string
 17 |   Company?: string
 18 |   Stage?: string
 19 |   Contact?: string
 20 |   Notes?: string
 21 | }
 22 | 
 23 | export async function fetchRecords() {
 24 |   try {
 25 |     const records = await base("Default").select({ view: "Grid view" }).all()
 26 |     const results = records.map((record) => ({
 27 |       id: record.id,
 28 |       company: record.fields.Company,
 29 |       stage: record.fields.Stage,
 30 |       contact: record.fields.Contact,
 31 |       notes: record.fields.Notes,
 32 |       email: record.fields.Email,
 33 |     }))
 34 |     return {
 35 |       data: results,
 36 |       error: null,
 37 |     }
 38 |   } catch (err) {
 39 |     console.error(err)
 40 |     return {
 41 |       data: null,
 42 |       error: err,
 43 |     }
 44 |   }
 45 | }
 46 | 
 47 | export async function createRecord(args: NewRecord) {
 48 |   try {
 49 |     const record = await base("Default").create({
 50 |       Email: args.Email,
 51 |       Company: args.Company,
 52 |       Stage: args.Stage,
 53 |       Contact: args.Contact,
 54 |       Notes: args.Notes,
 55 |     })
 56 |     return {
 57 |       data: record.id,
 58 |       error: null,
 59 |     }
 60 |   } catch (err) {
 61 |     console.error(err)
 62 |     return {
 63 |       data: null,
 64 |       error: err,
 65 |     }
 66 |   }
 67 | }
 68 | 
 69 | export async function updateRecord(id: string, args: UpdateRecord) {
 70 |   try {
 71 |     const record = await base("Default").update(id, {
 72 |       Email: args.Email || undefined,
 73 |       Company: args.Company || undefined,
 74 |       Stage: args.Stage || undefined,
 75 |       Contact: args.Contact || undefined,
 76 |       Notes: args.Notes || undefined,
 77 |     })
 78 |     return {
 79 |       data: record.id,
 80 |       error: null,
 81 |     }
 82 |   } catch (err) {
 83 |     console.error(err)
 84 |     return {
 85 |       data: null,
 86 |       error: err,
 87 |     }
 88 |   }
 89 | }
 90 | 
 91 | // createRecord({
 92 | //   Email: "[email protected]",
 93 | //   Company: "Aravind Srinivasan",
 94 | //   Stage: "Prospect",
 95 | //   Contact: "Aravind Srinivasan",
 96 | //   Notes: "AI search engine",
 97 | // })
 98 | 
 99 | // ;(async () => {
100 | //   const results = await fetchRecords()
101 | //   console.log(results)
102 | // })()
103 | 
104 | // updateRecord("rec9FCaEJEcis2fub", { Contact: "Amit Singh" })
105 | 
```

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

```bash
 1 | #!/usr/bin/env bash
 2 | set -eu
 3 | 
 4 | ##############################################################################
 5 | # Expose CLI Install Script
 6 | #
 7 | # This script downloads the latest stable 'expose-cli' binary from GitHub releases
 8 | # and installs it to your system.
 9 | #
10 | # Supported OS: macOS (darwin), Linux
11 | # Supported Architectures: x86_64, arm64
12 | #
13 | # Usage:
14 | #   curl -fsSL https://github.com/a0dotrun/expose/releases/download/stable/install.sh | bash
15 | #
16 | ##############################################################################
17 | 
18 | # --- 1) Check for curl ---
19 | if ! command -v curl >/dev/null 2>&1; then
20 |   echo "Error: 'curl' is required to download Expose CLI. Please install curl and try again."
21 |   exit 1
22 | fi
23 | 
24 | # --- 2) Detect OS/Architecture ---
25 | OS=$(uname -s | tr '[:upper:]' '[:lower:]')
26 | ARCH=$(uname -m)
27 | 
28 | case "$OS" in
29 |   linux) OS_ID="unknown-linux-gnu" ;;
30 |   darwin) OS_ID="apple-darwin" ;;
31 |   *)
32 |     echo "Error: Unsupported OS '$OS'. Expose CLI only supports Linux and macOS."
33 |     exit 1
34 |     ;;
35 | esac
36 | 
37 | case "$ARCH" in
38 |   x86_64|amd64)
39 |     ARCH_ID="x86_64"
40 |     ;;
41 |   arm64|aarch64)
42 |     ARCH_ID="aarch64"
43 |     ;;
44 |   *)
45 |     echo "Error: Unsupported architecture '$ARCH'. Expose CLI supports x86_64 and arm64."
46 |     exit 1
47 |     ;;
48 | esac
49 | 
50 | # --- 3) Set download URL ---
51 | REPO="a0dotrun/expose"
52 | INSTALL_DIR="${EXPOSE_BIN_DIR:-"$HOME/.local/bin"}"
53 | FILE="expose-cli-${ARCH_ID}-${OS_ID}.tar.bz2"
54 | DOWNLOAD_URL="https://github.com/$REPO/releases/download/stable/$FILE"
55 | 
56 | # --- 4) Download the binary ---
57 | echo "Downloading $FILE from $DOWNLOAD_URL..."
58 | curl -sLf "$DOWNLOAD_URL" --output "$FILE"
59 | 
60 | # --- 5) Extract & Install ---
61 | mkdir -p "$INSTALL_DIR"
62 | tar -xjf "$FILE" -C "$INSTALL_DIR"
63 | rm "$FILE"
64 | 
65 | # Ensure it's executable
66 | chmod +x "$INSTALL_DIR/expose-cli"
67 | 
68 | echo "✅ Expose CLI installed successfully at $INSTALL_DIR/expose-cli"
69 | 
70 | # --- 6) Add to PATH if needed ---
71 | if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
72 |   echo ""
73 |   echo "Warning: Expose CLI installed, but $INSTALL_DIR is not in your PATH."
74 |   echo "Add it by running:"
75 |   echo "    export PATH=\"$INSTALL_DIR:\$PATH\""
76 |   echo "Then reload your shell (e.g. 'source ~/.bashrc', 'source ~/.zshrc')."
77 | fi
78 | 
79 | echo "Run 'expose-cli --help' to get started!"
80 | 
```

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

```yaml
 1 | # This workflow is main release, needs to be manually tagged & pushed.
 2 | on:
 3 |   push:
 4 |     paths-ignore:
 5 |       - "docs/**"
 6 |     tags:
 7 |       - "v*"
 8 | 
 9 | name: Release
10 | concurrency:
11 |   group: ${{ github.workflow }}-${{ github.ref }}
12 |   cancel-in-progress: true
13 | 
14 | jobs:
15 |   # ------------------------------------
16 |   # 1) Build CLI for multiple OS/Arch (Linux, MacOS)
17 |   # ------------------------------------
18 |   build-cli:
19 |     uses: ./.github/workflows/build-cli.yml
20 | 
21 |   # ------------------------------------
22 |   # 2) Upload Install CLI Script
23 |   # ------------------------------------
24 |   install-script:
25 |     name: Upload Install Script
26 |     runs-on: ubuntu-latest
27 |     needs: [build-cli]
28 |     steps:
29 |       - uses: actions/checkout@v4
30 |       - uses: actions/upload-artifact@v4
31 |         with:
32 |           name: download_cli.sh
33 |           path: download_cli.sh
34 | 
35 |   # ------------------------------------
36 |   # 3) Create/Update GitHub Release CLI
37 |   # ------------------------------------
38 |   release-cli:
39 |     name: Release CLI
40 |     runs-on: ubuntu-latest
41 |     needs: [build-cli, install-script]
42 |     permissions:
43 |       contents: write
44 |     steps:
45 |       - name: Download all artifacts
46 |         uses: actions/download-artifact@v4
47 |         with:
48 |           merge-multiple: true
49 | 
50 |       - name: Release versioned
51 |         uses: ncipollo/release-action@v1
52 |         with:
53 |           token: ${{ secrets.GITHUB_TOKEN }}
54 |           artifacts: |
55 |             expose-cli-*.tar.bz2
56 |             download_cli.sh
57 |           allowUpdates: true
58 |           omitBody: true
59 |           omitPrereleaseDuringUpdate: true
60 | 
61 |       - name: Release CLI Stable
62 |         uses: ncipollo/release-action@v1
63 |         with:
64 |           tag: stable
65 |           name: Stable
66 |           token: ${{ secrets.GITHUB_TOKEN }}
67 |           artifacts: |
68 |             expose-cli-*.tar.bz2
69 |             download_cli.sh
70 |           allowUpdates: true
71 |           omitBody: true
72 |           omitPrereleaseDuringUpdate: true
73 |   # ------------------------------------
74 |   # 4) Publish NPM Package
75 |   # ------------------------------------
76 |   publish-npm:
77 |     name: Publish NPM Package
78 |     runs-on: ubuntu-latest
79 |     steps:
80 |       - name: Checkout repository
81 |         uses: actions/checkout@v3
82 | 
83 |       - name: Setup Bun
84 |         uses: oven-sh/setup-bun@v2
85 | 
86 |       - name: Install dependencies
87 |         run: bun install
88 | 
89 |       - name: Authenticate NPM
90 |         run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc
91 | 
92 |       - name: Run publish
93 |         run: make publish
94 |         env:
95 |           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96 |           NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
97 | 
```

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

```yaml
  1 | # This is a **reuseable** workflow that bundles the CLI
  2 | # It doesn't get triggered on its own. It gets used in multiple workflows:
  3 | #  - release.yml
  4 | on:
  5 |   workflow_call:
  6 |     inputs:
  7 |       version:
  8 |         required: false
  9 |         default: ""
 10 |         type: string
 11 |       # Let's allow overriding the OSes and architectures in JSON array form:
 12 |       # e.g. '["ubuntu-latest","macos-latest"]'
 13 |       # If no input is provided, these defaults apply.
 14 |       operating-systems:
 15 |         type: string
 16 |         required: false
 17 |         default: '["ubuntu-latest","macos-latest"]'
 18 |       architectures:
 19 |         type: string
 20 |         required: false
 21 |         default: '["x86_64","aarch64"]'
 22 | 
 23 | name: "Reusable workflow to build CLI"
 24 | 
 25 | jobs:
 26 |   build-cli:
 27 |     name: Build CLI
 28 |     runs-on: ${{ matrix.os }}
 29 |     strategy:
 30 |       fail-fast: false
 31 |       matrix:
 32 |         os: ${{ fromJson(inputs.operating-systems) }}
 33 |         architecture: ${{ fromJson(inputs.architectures) }}
 34 |         include:
 35 |           - os: ubuntu-latest
 36 |             target-suffix: unknown-linux-gnu
 37 |             goos: linux
 38 |             os_name: linux
 39 |           - os: macos-latest
 40 |             target-suffix: apple-darwin
 41 |             goos: darwin
 42 |             os_name: macos
 43 | 
 44 |     steps:
 45 |       - name: Checkout code
 46 |         uses: actions/checkout@v4
 47 | 
 48 |       - name: Set up Go
 49 |         uses: actions/setup-go@v5
 50 |         with:
 51 |           go-version: stable
 52 | 
 53 |       - name: Install jq
 54 |         run: |
 55 |           if [[ "${{ matrix.os }}" == "ubuntu-latest" ]]; then
 56 |             sudo apt-get update && sudo apt-get install -y jq
 57 |           elif [[ "${{ matrix.os }}" == "macos-latest" ]]; then
 58 |             brew install jq
 59 |           fi
 60 | 
 61 |       - name: Extract version
 62 |         run: |
 63 |           VERSION=$(jq -r '.version' packages/expose/package.json)
 64 |           echo "VERSION=$VERSION" >> $GITHUB_ENV
 65 |           echo "Extracted version: $VERSION"
 66 | 
 67 |       - name: Set GOARCH
 68 |         run: |
 69 |           if [[ "${{ matrix.architecture }}" == "x86_64" ]]; then
 70 |             echo "GOARCH=amd64" >> $GITHUB_ENV
 71 |           elif [[ "${{ matrix.architecture }}" == "aarch64" ]]; then
 72 |             echo "GOARCH=arm64" >> $GITHUB_ENV
 73 |           fi
 74 | 
 75 |       - name: Build binary
 76 |         env:
 77 |           GOOS: ${{ matrix.goos }}
 78 |           GOARCH: ${{ env.GOARCH }}
 79 |         run: |
 80 |           TARGET="${{ matrix.architecture }}-${{ matrix.target-suffix }}"
 81 |           echo "Building for target: ${TARGET} (GOOS=$GOOS, GOARCH=$GOARCH)"
 82 | 
 83 |           # Create output directory
 84 |           mkdir -p build/${TARGET}/release
 85 | 
 86 |           # Build the binary
 87 |           go build -o build/${TARGET}/release/expose-cli cmd/cli.go
 88 | 
 89 |           # Create tarball
 90 |           cd build/${TARGET}/release
 91 |           tar -cjf expose-cli-${TARGET}.tar.bz2 expose-cli
 92 |           cd -
 93 | 
 94 |           # Set artifact path for upload step
 95 |           echo "ARTIFACT=build/${TARGET}/release/expose-cli-${TARGET}.tar.bz2" >> $GITHUB_ENV
 96 | 
 97 |           echo "Build complete: ${{ env.ARTIFACT }}"
 98 | 
 99 |       - name: Upload artifact
100 |         uses: actions/upload-artifact@v4
101 |         with:
102 |           name: expose-cli-${{ matrix.architecture }}-${{ matrix.target-suffix }}
103 |           path: ${{ env.ARTIFACT }}
104 | 
```

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

```typescript
  1 | import {
  2 |   CallToolRequestSchema,
  3 |   CallToolResult,
  4 |   ErrorCode,
  5 |   InitializeRequestSchema,
  6 |   InitializeResult,
  7 |   JSONRPCError,
  8 |   JSONRPCRequest,
  9 |   JSONRPCResponse,
 10 |   ListToolsRequestSchema,
 11 |   ListToolsResult,
 12 | } from "@modelcontextprotocol/sdk/types.js"
 13 | import { z } from "zod"
 14 | import { zodToJsonSchema } from "zod-to-json-schema"
 15 | import { Tool } from "./tool.js"
 16 | 
 17 | class MCPError extends Error {
 18 |   constructor(
 19 |     message: string,
 20 |     public code: ErrorCode,
 21 |   ) {
 22 |     super(message)
 23 |   }
 24 | }
 25 | 
 26 | const RequestSchema = z.union([
 27 |   InitializeRequestSchema,
 28 |   ListToolsRequestSchema,
 29 |   CallToolRequestSchema,
 30 | ])
 31 | 
 32 | type RequestSchema = z.infer<typeof RequestSchema>
 33 | 
 34 | export function createMcp(input: { tools: Tool[] }) {
 35 |   return {
 36 |     async process(message: JSONRPCRequest) {
 37 |       try {
 38 |         const parsed = RequestSchema.parse(message)
 39 |         return await (async (): Promise<JSONRPCResponse> => {
 40 |           if (parsed.method === "initialize")
 41 |             return {
 42 |               jsonrpc: "2.0",
 43 |               id: message.id,
 44 |               result: {
 45 |                 protocolVersion: "2024-11-05",
 46 |                 capabilities: {
 47 |                   tools: {},
 48 |                 },
 49 |                 serverInfo: {
 50 |                   name: "expose",
 51 |                   version: "0.0.1",
 52 |                 },
 53 |               } as InitializeResult,
 54 |             }
 55 | 
 56 |           if (parsed.method === "tools/list") {
 57 |             return {
 58 |               jsonrpc: "2.0",
 59 |               id: message.id,
 60 |               result: {
 61 |                 tools: input.tools.map((tool) => ({
 62 |                   name: tool.name,
 63 |                   inputSchema: tool.args
 64 |                     ? (zodToJsonSchema(tool.args as any, "args").definitions![
 65 |                         "args"
 66 |                       ] as any)
 67 |                     : { type: "object" },
 68 |                   description: tool.description,
 69 |                 })),
 70 |               } as ListToolsResult,
 71 |             } satisfies JSONRPCResponse
 72 |           }
 73 | 
 74 |           if (parsed.method === "tools/call") {
 75 |             const tool = input.tools.find(
 76 |               (tool) => tool.name === parsed.params.name,
 77 |             )
 78 |             if (!tool)
 79 |               throw new MCPError("Tool not found", ErrorCode.MethodNotFound)
 80 | 
 81 |             let args = parsed.params.arguments
 82 |             if (tool.args) {
 83 |               const validated = await tool.args["~standard"].validate(args)
 84 |               if (validated.issues)
 85 |                 throw new MCPError("Invalid arguments", ErrorCode.InvalidParams)
 86 |               args = validated.value as any
 87 |             }
 88 | 
 89 |             return tool
 90 |               .run(args)
 91 |               .catch(
 92 |                 (error) =>
 93 |                   ({
 94 |                     jsonrpc: "2.0",
 95 |                     id: message.id,
 96 |                     error: {
 97 |                       code: ErrorCode.InternalError,
 98 |                       message: error.message,
 99 |                     },
100 |                   }) satisfies JSONRPCError,
101 |               )
102 |               .then(
103 |                 (result) =>
104 |                   ({
105 |                     jsonrpc: "2.0",
106 |                     id: message.id,
107 |                     result: {
108 |                       content: [
109 |                         {
110 |                           type: "text",
111 |                           text: JSON.stringify(result, null, 2),
112 |                         },
113 |                       ],
114 |                     } as CallToolResult,
115 |                   }) satisfies JSONRPCResponse,
116 |               )
117 |           }
118 |           throw new MCPError("Method not found", ErrorCode.MethodNotFound)
119 |         })()
120 |       } catch (error) {
121 |         if (error instanceof MCPError) {
122 |           const code = error.code
123 |           return {
124 |             jsonrpc: "2.0",
125 |             id: message.id,
126 |             error: { code, message: error.message },
127 |           } satisfies JSONRPCError
128 |         }
129 |         return {
130 |           jsonrpc: "2.0",
131 |           id: message.id,
132 |           error: {
133 |             code: ErrorCode.InternalError,
134 |             message: "Internal error",
135 |           },
136 |         } satisfies JSONRPCError
137 |       }
138 |     },
139 |   }
140 | }
141 | 
```

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

```go
  1 | package expose
  2 | 
  3 | import (
  4 | 	"bufio"
  5 | 	"bytes"
  6 | 	"context"
  7 | 	"encoding/json"
  8 | 	"fmt"
  9 | 	"io"
 10 | 	"log"
 11 | 	"net/http"
 12 | 	"os"
 13 | 	"os/signal"
 14 | 	"syscall"
 15 | 	"time"
 16 | 
 17 | 	"github.com/mark3labs/mcp-go/mcp"
 18 | )
 19 | 
 20 | type Client struct {
 21 | 	baseURL    string
 22 | 	httpClient *http.Client
 23 | 	headers    map[string]string
 24 | }
 25 | 
 26 | func NewClient(baseURL string, timeout time.Duration) *Client {
 27 | 	return &Client{
 28 | 		baseURL: baseURL,
 29 | 		httpClient: &http.Client{
 30 | 			Timeout: timeout,
 31 | 		},
 32 | 		headers: map[string]string{
 33 | 			"Content-Type": "application/json",
 34 | 		},
 35 | 	}
 36 | }
 37 | 
 38 | func (c *Client) SetHeader(key, value string) {
 39 | 	c.headers[key] = value
 40 | }
 41 | 
 42 | func (c *Client) MakePostRequest(
 43 | 	ctx context.Context,
 44 | 	endpoint string,
 45 | 	body interface{},
 46 | ) ([]byte, error) {
 47 | 	url := fmt.Sprintf("%s%s", c.baseURL, endpoint)
 48 | 	var requestBody []byte
 49 | 	var err error
 50 | 	if body != nil {
 51 | 		requestBody, err = json.Marshal(body)
 52 | 		if err != nil {
 53 | 			return nil, fmt.Errorf("failed to marshal request body: %w", err)
 54 | 		}
 55 | 	}
 56 | 
 57 | 	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(requestBody))
 58 | 	if err != nil {
 59 | 		return nil, fmt.Errorf("failed to create request: %w", err)
 60 | 	}
 61 | 
 62 | 	for key, value := range c.headers {
 63 | 		req.Header.Set(key, value)
 64 | 	}
 65 | 
 66 | 	resp, err := c.httpClient.Do(req)
 67 | 	if err != nil {
 68 | 		return nil, fmt.Errorf("failed to send request: %w", err)
 69 | 	}
 70 | 	defer resp.Body.Close()
 71 | 
 72 | 	responseBody, err := io.ReadAll(resp.Body)
 73 | 	if err != nil {
 74 | 		return nil, fmt.Errorf("failed to read response body: %w", err)
 75 | 	}
 76 | 
 77 | 	return responseBody, nil
 78 | }
 79 | 
 80 | func createErrorResponse(
 81 | 	id interface{},
 82 | 	code int,
 83 | 	message string,
 84 | ) mcp.JSONRPCMessage {
 85 | 	return mcp.JSONRPCError{
 86 | 		JSONRPC: mcp.JSONRPC_VERSION,
 87 | 		ID:      id,
 88 | 		Error: struct {
 89 | 			Code    int         `json:"code"`
 90 | 			Message string      `json:"message"`
 91 | 			Data    interface{} `json:"data,omitempty"`
 92 | 		}{
 93 | 			Code:    code,
 94 | 			Message: message,
 95 | 		},
 96 | 	}
 97 | }
 98 | 
 99 | func createResponse(id interface{}, result interface{}) mcp.JSONRPCMessage {
100 | 	return mcp.JSONRPCResponse{
101 | 		JSONRPC: mcp.JSONRPC_VERSION,
102 | 		ID:      id,
103 | 		Result:  result,
104 | 	}
105 | }
106 | 
107 | type ProxyStdioServer struct {
108 | 	BaseURL   string
109 | 	Client    *Client
110 | 	errLogger *log.Logger
111 | }
112 | 
113 | func NewProxyStdioServer(baseURL string, timeout time.Duration) *ProxyStdioServer {
114 | 	return &ProxyStdioServer{
115 | 		BaseURL:   baseURL,
116 | 		Client:    NewClient(baseURL, timeout),
117 | 		errLogger: log.New(os.Stderr, "", log.LstdFlags),
118 | 	}
119 | }
120 | 
121 | func (s *ProxyStdioServer) SetBaseURL(baseURL string) {
122 | 	s.BaseURL = baseURL
123 | }
124 | 
125 | func (s *ProxyStdioServer) SetErrLogger(errLogger *log.Logger) {
126 | 	s.errLogger = errLogger
127 | }
128 | 
129 | func (s *ProxyStdioServer) Listen(
130 | 	ctx context.Context,
131 | 	stdin io.Reader,
132 | 	stdout io.Writer,
133 | ) error {
134 | 
135 | 	reader := bufio.NewReader(stdin)
136 | 
137 | 	for {
138 | 		select {
139 | 		case <-ctx.Done():
140 | 			return ctx.Err()
141 | 		default:
142 | 			// Use a goroutine to make the read cancellable
143 | 			readChan := make(chan string, 1)
144 | 			errChan := make(chan error, 1)
145 | 
146 | 			go func() {
147 | 				line, err := reader.ReadString('\n')
148 | 				if err != nil {
149 | 					errChan <- err
150 | 					return
151 | 				}
152 | 				readChan <- line
153 | 			}()
154 | 
155 | 			select {
156 | 			case <-ctx.Done():
157 | 				return ctx.Err()
158 | 			case err := <-errChan:
159 | 				if err == io.EOF {
160 | 					return nil
161 | 				}
162 | 				s.errLogger.Printf("Error reading input: %v", err)
163 | 				return err
164 | 			case line := <-readChan:
165 | 				if err := s.proxyMessage(ctx, line, stdout); err != nil {
166 | 					if err == io.EOF {
167 | 						return nil
168 | 					}
169 | 					s.errLogger.Printf("Error handling message: %v", err)
170 | 					return err
171 | 				}
172 | 			}
173 | 		}
174 | 	}
175 | }
176 | 
177 | func (s *ProxyStdioServer) handleMessage(
178 | 	ctx context.Context,
179 | 	message json.RawMessage,
180 | ) mcp.JSONRPCMessage {
181 | 	var baseMessage struct {
182 | 		JSONRPC string      `json:"jsonrpc"`
183 | 		Method  string      `json:"method"`
184 | 		ID      interface{} `json:"id,omitempty"`
185 | 	}
186 | 
187 | 	if err := json.Unmarshal(message, &baseMessage); err != nil {
188 | 		return createErrorResponse(nil, mcp.PARSE_ERROR, "Failed to parse message")
189 | 	}
190 | 
191 | 	// Check for valid JSONRPC version
192 | 	if baseMessage.JSONRPC != mcp.JSONRPC_VERSION {
193 | 		return createErrorResponse(
194 | 			baseMessage.ID,
195 | 			mcp.INVALID_REQUEST,
196 | 			"Invalid JSON-RPC version",
197 | 		)
198 | 	}
199 | 
200 | 	if baseMessage.ID == nil {
201 | 		return nil
202 | 	}
203 | 
204 | 	if baseMessage.Method == "ping" {
205 | 		var request mcp.PingRequest
206 | 		if err := json.Unmarshal(message, &request); err != nil {
207 | 			return createErrorResponse(
208 | 				baseMessage.ID,
209 | 				mcp.INVALID_REQUEST,
210 | 				"Invalid ping request",
211 | 			)
212 | 		}
213 | 		return createResponse(baseMessage.ID, mcp.EmptyResult{})
214 | 	}
215 | 
216 | 	switch baseMessage.Method {
217 | 	case "initialize", "tools/list", "tools/call":
218 | 		response, err := s.Client.MakePostRequest(ctx, "/", message)
219 | 		if err != nil {
220 | 			return createErrorResponse(
221 | 				baseMessage.ID,
222 | 				mcp.INTERNAL_ERROR,
223 | 				"Failed to make request",
224 | 			)
225 | 		}
226 | 		var responseMessage mcp.JSONRPCMessage
227 | 		if err := json.Unmarshal(response, &responseMessage); err != nil {
228 | 			return createErrorResponse(
229 | 				baseMessage.ID,
230 | 				mcp.INTERNAL_ERROR,
231 | 				"Failed to parse response",
232 | 			)
233 | 		}
234 | 		return responseMessage
235 | 	default:
236 | 		return createErrorResponse(
237 | 			baseMessage.ID,
238 | 			mcp.METHOD_NOT_FOUND,
239 | 			fmt.Sprintf("Method %s not supported", baseMessage.Method),
240 | 		)
241 | 	}
242 | }
243 | 
244 | func (s *ProxyStdioServer) proxyMessage(
245 | 	ctx context.Context,
246 | 	line string,
247 | 	writer io.Writer,
248 | ) error {
249 | 	var rawMessage json.RawMessage
250 | 	if err := json.Unmarshal([]byte(line), &rawMessage); err != nil {
251 | 		response := createErrorResponse(nil, mcp.PARSE_ERROR, "Parse error")
252 | 		return s.writeResponse(response, writer)
253 | 	}
254 | 
255 | 	response := s.handleMessage(ctx, rawMessage)
256 | 	if response != nil {
257 | 		return s.writeResponse(response, writer)
258 | 	}
259 | 	return nil
260 | }
261 | 
262 | // writeResponse marshals and writes a JSON-RPC response message followed by a newline.
263 | // Returns an error if marshaling or writing fails.
264 | func (s *ProxyStdioServer) writeResponse(
265 | 	response mcp.JSONRPCMessage,
266 | 	writer io.Writer,
267 | ) error {
268 | 	responseBytes, err := json.Marshal(response)
269 | 	if err != nil {
270 | 		return err
271 | 	}
272 | 
273 | 	// Write response followed by newline
274 | 	if _, err := fmt.Fprintf(writer, "%s\n", responseBytes); err != nil {
275 | 		return err
276 | 	}
277 | 
278 | 	return nil
279 | }
280 | 
281 | func ProxyServeStdio(baseURL string, timeout time.Duration) error {
282 | 	ps := NewProxyStdioServer(baseURL, timeout)
283 | 	ps.SetErrLogger(log.New(os.Stderr, "", log.LstdFlags))
284 | 
285 | 	ctx, cancel := context.WithCancel(context.Background())
286 | 	defer cancel()
287 | 
288 | 	sigChan := make(chan os.Signal, 1)
289 | 	signal.Notify(sigChan, syscall.SIGTERM, syscall.SIGINT)
290 | 
291 | 	go func() {
292 | 		<-sigChan
293 | 		cancel()
294 | 	}()
295 | 
296 | 	return ps.Listen(ctx, os.Stdin, os.Stdout)
297 | }
298 | 
```