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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@ Available MCP options:

- `--prefix path` - Specify the directory to find the `package.json` file

### HTTP MCP via Vite Plugin

Tidewave also provides HTTP-based MCP access through a Vite plugin for
development environments. Add the plugin to your `vite.config.js`:

```javascript
import { defineConfig } from 'vite';
import tidewave from 'tidewave/vite-plugin';

export default defineConfig({
plugins: [tidewave()],
});
```

This exposes MCP endpoints at `/tidewave/mcp` and `/tidewave/shell` during
development.

Configuration options:

```javascript
tidewave({
allowRemoteAccess: false, // Allow access from remote IPs
allowedOrigins: ['localhost'], // Allowed origins: defaults to the Vite's host+port
});
```

### CLI Usage

Tidewave also provides the MCP features over a CLI tool. Use it directly via
Expand Down
197 changes: 170 additions & 27 deletions bun.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export default [
// Function style preferences
'prefer-arrow-callback': 'error',
'arrow-body-style': ['error', 'as-needed'],
'func-style': ['error', 'declaration'], // Prefer function declarations for better stack traces
// Prefer function declarations for better stack traces
'func-style': ['error', 'declaration', { allowArrowFunctions: true }],
},
},
{
Expand Down
21 changes: 11 additions & 10 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05-small";
};

outputs = {
nixpkgs,
...
}: let
outputs = {nixpkgs, ...}: let
inherit (nixpkgs.lib) genAttrs;
inherit (nixpkgs.lib.systems) flakeExposed;
forAllSystems = f:
Expand All @@ -20,12 +17,16 @@
in {
default = mkShell {
name = "tidewave-javascript";
packages = with pkgs; [
bun
# nodejs_24
# nodePackages.npm
typescript
] ++ lib.optional stdenv.isLinux [inotify-tools];
packages = with pkgs;
[
bun
# with homebrew
# nodejs_24
# nodePackages.npm
typescript
just
]
++ lib.optional stdenv.isLinux [inotify-tools];
};
});

Expand Down
22 changes: 22 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
curl-mcp body='{}' port='5001':
curl -X POST \
-H 'accept: application/json,text/event-stream' \
-H 'content-type: application/json' \
-d '{{body}}' \
http://localhost:{{port}}/tidewave/mcp

initialize-mcp:
@just curl-mcp '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2025-03-26","clientInfo":{"name":"curl-test","version":"1.0.0"},"capabilities":{}},"id":1}'

list-tools-mcp port='5001':
@just curl-mcp '{"jsonrpc":"2.0","method":"tools/list","params":{},"id":2}' {{port}}

curl-shell command port='5001':
curl -X POST http://localhost:{{port}}/tidewave/shell \
-H 'content-type: application/json' \
-H 'accept: text/event-stream,application/json' \
-d '{"command": "{{command}}"}' \
--output -

default:
@just --choose
117 changes: 115 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"CHANGELOG.md"
],
"scripts": {
"dist": "bun build --outdir dist --root src --external commander --external chalk --external typescript src/**/*.ts --target node",
"dist": "bun build --outdir dist --root src --external commander --external chalk --external typescript src/**/*.ts src/*.ts --target node",
"dev": "bun run src/cli/index.ts",
"test": "bun test",
"lint": "eslint src/ --ext .ts,.js",
Expand All @@ -41,14 +41,18 @@
},
"dependencies": {
"@modelcontextprotocol/sdk": "^1.17.4",
"body-parser": "^2.2.0",
"chalk": "^5.3.0",
"commander": "^12.1.0",
"connect": "^3.7.0",
"typescript": "^5",
"zod": "3.25.76"
},
"devDependencies": {
"@eslint/js": "^9.16.0",
"@types/body-parser": "^1.19.6",
"@types/bun": "latest",
"@types/connect": "^3.4.38",
"@types/node": "^22.10.1",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
Expand All @@ -57,6 +61,7 @@
"eslint": "^9.16.0",
"globals": "^15.14.0",
"prettier": "^3.4.2",
"vite": "^7.1.5",
"vitest": "^3.2.4"
},
"peerDependencies": {
Expand Down
10 changes: 8 additions & 2 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,15 @@ async function handleGetSourcePath(
console.log(sourceResult.path);
}

async function handleMcp(options: { prefix?: string }): Promise<void> {
interface McpOptions {
prefix?: string;
}

async function handleMcp(options: McpOptions): Promise<void> {
if (options.prefix) chdir(options.prefix);
console.error('Starting tidewave MCP server using stdio');

console.log('[Tidewave] Starting MCP server');

const transport = new StdioServerTransport(process.stdin, process.stdout);
await serveMcp(transport);
}
Expand Down
2 changes: 1 addition & 1 deletion src/evaluation/eval_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { EvaluationRequest } from '../core';

process.on('message', async ({ code, args }: EvaluationRequest) => {
if (!process.send) {
console.error('Unable to establish communication channel with code-executor.');
console.error('[Tidewave] Unable to establish communication channel with code-executor.');
process.exit(1);
}

Expand Down
52 changes: 52 additions & 0 deletions src/http/handlers/mcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { methodNotAllowed, type Request, type Response, type NextFn } from '..';
import { serveMcp } from '../../mcp';

export async function handleMcp(req: Request, res: Response, next: NextFn): Promise<void> {
try {
if (req.method !== 'POST') {
methodNotAllowed(res);
Comment thread
zoedsoupe marked this conversation as resolved.
return;
}

console.debug(`[Tidewave] Received ${req.method} message`);

// stateless mode, no session managament
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true,
});

if (!(req.body || res.headersSent)) {
mcpErrorResponse(res, 'Request body was not parsed');
return;
}

await serveMcp(transport);
await transport.handleRequest(req, res, req.body);
} catch (e) {
console.error(`[Tidewave] Failed to serve MCP with ${e}`);

if (!res.headersSent) {
mcpErrorResponse(res, e);
}

next(e);
}
}

function mcpErrorResponse(res: Response, err: Error | unknown): void {
res.statusCode = 500;
res.setHeader('content-type', 'application/json');
res.end(
JSON.stringify({
jsonrpc: 2.0,
id: null,
error: {
code: -32603,
message: 'Internal server error',
data: err instanceof Error ? err.message : String(err),
},
}),
);
}
Loading