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
10 changes: 10 additions & 0 deletions javascript/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,13 @@ Run the following command:
```bash
npx forevervm-mcp install --claude
```

For other MCP clients, see [the docs](https://forevervm.com/docs/guides/forevervm-mcp-server/).

## Installing locally (for development only)

In the MCP client, set the command to `npm` and the arguments to:

```json
["--prefix", "<path/to/this/directory>", "run", "start", "run"]
```
117 changes: 65 additions & 52 deletions javascript/mcp-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,25 @@

import { Server } from '@modelcontextprotocol/sdk/server/index.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js'
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ListPromptsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { ForeverVM } from '@forevervm/sdk'
import fs from 'fs'
import path from 'path'
import os from 'os'
import { Command } from 'commander'
import { installForClaude } from './install/claude.js'
import { installForWindsurf } from './install/windsurf.js'
import { installForGoose } from './install/goose.js'
import { installForeverVM } from './install/index.js'

function installForeverVM(options: { claude: boolean; windsurf: boolean; goose: boolean }) {
let forevervmToken = getForeverVMToken()
const DEFAULT_FOREVERVM_SERVER = 'https://api.forevervm.com'

if (!forevervmToken) {
console.error(
'ForeverVM token not found. Please set up ForeverVM first by running `npx forevervm login` or `npx forevervm signup`.',
)
process.exit(1)
}

if (!options.claude && !options.windsurf && !options.goose) {
console.log(
'Select at least one MCP client to install. Available options: --claude, --windsurf, --goose',
)
process.exit(1)
}

if (options.claude) {
installForClaude()
}

if (options.goose) {
installForGoose()
}

if (options.windsurf) {
installForWindsurf()
}
interface ForeverVMOptions {
token?: string
baseUrl?: string
}

// Zod schema
Expand All @@ -52,9 +32,12 @@ const ExecMachineSchema = z.object({
const RUN_REPL_TOOL_NAME = 'run-python-in-repl'
const CREATE_REPL_MACHINE_TOOL_NAME = 'create-python-repl'

function getForeverVMToken(): string | null {
export function getForeverVMOptions(): ForeverVMOptions {
if (process.env.FOREVERVM_TOKEN) {
return process.env.FOREVERVM_TOKEN
return {
token: process.env.FOREVERVM_TOKEN,
baseUrl: process.env.FOREVERVM_BASE_URL || DEFAULT_FOREVERVM_SERVER,
}
}

const configFilePath = path.join(os.homedir(), '.config', 'forevervm', 'config.json')
Expand All @@ -72,7 +55,16 @@ function getForeverVMToken(): string | null {
console.error('ForeverVM config file does not contain a token')
process.exit(1)
}
return config.token

let baseUrl = config.server_url || DEFAULT_FOREVERVM_SERVER

// remove trailing slash
baseUrl = baseUrl.replace(/\/$/, '')

return {
token: config.token,
baseUrl,
}
} catch (error) {
console.error('Failed to read ForeverVM config file:', error)
process.exit(1)
Expand All @@ -88,16 +80,16 @@ interface ExecReplResponse {
image?: string
}
async function makeExecReplRequest(
forevervmToken: string,
forevervmOptions: ForeverVMOptions,
pythonCode: string,
replId: string,
): Promise<ExecReplResponse> {
try {
const fvm = new ForeverVM({ token: forevervmToken })
const fvm = new ForeverVM(forevervmOptions)

const repl = await fvm.repl(replId)

const execResult = await repl.exec(pythonCode, { timeoutSeconds: 5 })
const execResult = await repl.exec(pythonCode)

const output: string[] = []
for await (const nextOutput of execResult.output) {
Expand Down Expand Up @@ -138,38 +130,51 @@ async function makeExecReplRequest(
}
}
} catch (error: any) {
console.error(`Failed to execute code on the ForeverVM REPL: ${error} \n\nreplId: ${replId}`)
process.exit(1)
return {
error: `Failed to execute Python code: ${error}`,
output: '',
result: '',
replId: replId,
}
}
}

async function makeCreateMachineRequest(forevervmToken: string): Promise<string> {
async function makeCreateMachineRequest(forevervmOptions: ForeverVMOptions): Promise<string> {
try {
const fvm = new ForeverVM({ token: forevervmToken })
console.error('using options', forevervmOptions)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug logging alert: Consider removing or replacing the console.error debug statement printing options with a proper logger.

const fvm = new ForeverVM(forevervmOptions)

const machine = await fvm.createMachine()

return machine.machine_name
} catch (error: any) {
console.error(`Failed to create ForeverVM machine: ${error}`)
process.exit(1)
throw new Error(`Failed to create ForeverVM machine: ${error}`)
}
}

// Start server
async function runMCPServer() {
let forevervmToken = getForeverVMToken()

if (!forevervmToken) {
console.error('ForeverVM token not found. Please set up ForeverVM first.')
process.exit(1)
}
const forevervmOptions = getForeverVMOptions()

const server = new Server(
{ name: 'forevervm', version: '1.0.0' },
{ capabilities: { tools: {} } },
{ capabilities: { tools: {}, resources: {}, prompts: {} } },
)

// List resources
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [], // No resources currently available
}
})

// List prompts
server.setRequestHandler(ListPromptsRequestSchema, async () => {
return {
prompts: [], // No prompts currently available
}
})

// List tools
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
Expand Down Expand Up @@ -215,7 +220,7 @@ async function runMCPServer() {
try {
if (name === RUN_REPL_TOOL_NAME) {
const { pythonCode, replId } = ExecMachineSchema.parse(args)
const execResponse = await makeExecReplRequest(forevervmToken, pythonCode, replId)
const execResponse = await makeExecReplRequest(forevervmOptions, pythonCode, replId)

if (execResponse.error) {
return {
Expand Down Expand Up @@ -250,7 +255,15 @@ async function runMCPServer() {
],
}
} else if (name === CREATE_REPL_MACHINE_TOOL_NAME) {
const replId = await makeCreateMachineRequest(forevervmToken)
let replId
try {
replId = await makeCreateMachineRequest(forevervmOptions)
} catch (error) {
return {
content: [{ type: 'text', text: `Failed to create machine: ${error}` }],
isError: true,
}
}
return {
content: [
{
Expand Down
34 changes: 34 additions & 0 deletions javascript/mcp-server/src/install/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { getForeverVMOptions } from '../index.js'
import { installForClaude } from './claude.js'
import { installForGoose } from './goose.js'
import { installForWindsurf } from './windsurf.js'

export function installForeverVM(options: { claude: boolean; windsurf: boolean; goose: boolean }) {
const forevervmOptions = getForeverVMOptions()

if (!forevervmOptions?.token) {
console.error(
'ForeverVM token not found. Please set up ForeverVM first by running `npx forevervm login` or `npx forevervm signup`.',
)
process.exit(1)
}

if (!options.claude && !options.windsurf && !options.goose) {
console.log(
'Select at least one MCP client to install. Available options: --claude, --windsurf, --goose',
)
process.exit(1)
}

if (options.claude) {
installForClaude()
}

if (options.goose) {
installForGoose()
}

if (options.windsurf) {
installForWindsurf()
}
}
15 changes: 10 additions & 5 deletions javascript/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,16 @@ export class ForeverVM {
}

async #post(path: string, body?: object) {
const response = await fetch(`${this.#baseUrl}${path}`, {
method: 'POST',
headers: { ...this.#headers, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
})
let response
try {
response = await fetch(`${this.#baseUrl}${path}`, {
method: 'POST',
headers: { ...this.#headers, 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
})
} catch (error) {
throw new Error(`Failed to fetch: ${error}`)
Comment thread
paulgb marked this conversation as resolved.
}
if (!response.ok) {
const text = await response.text().catch(() => 'Unknown error')
throw new Error(`HTTP ${response.status}: ${text}`)
Expand Down
4 changes: 2 additions & 2 deletions rust/Cargo.lock

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

Loading