Turning a Simple JS Function into a Cross‑Platform AI Tool with MCP

This article details how we built an AI‑tool ecosystem by evolving a basic online JS cloud‑function platform into a unified, reusable capability layer that integrates with Flowise, LangChain StructuredTool, and the Model Context Protocol (MCP) to provide secure, cross‑platform tool calls for agents.

Bilibili Tech
Bilibili Tech
Bilibili Tech
Turning a Simple JS Function into a Cross‑Platform AI Tool with MCP

Introduction

Over the past year we experimented with building an AI‑tool ecosystem, gradually shaping a clear "capability production line" that turns a plain JavaScript function into a tool usable by any AI agent.

Why an AI Tool Capability Layer?

When developing internal AI applications (customer‑service bots, live‑stream assistants, etc.) we repeatedly faced four pain points:

Each new AI integration required rewriting the same tool logic.

AI needs a "capability" rather than a raw HTTP interface.

Raw JSON responses are hard for LLMs to consume reliably.

Tools were tightly coupled to specific projects and could not be shared as assets.

The core problem was the lack of a unified layer for writing, managing, publishing, and reusing tools.

First Version – StructuredTool (Flowise)

We started by creating a StructuredTool inside Flowise, an open‑source visual AI workflow platform built on LangChain’s TypeScript version. The tool looked like this:

export class QueryLiveRoomTool extends StructuredTool {
  name = 'QueryLiveRoomTool';
  description = '根据 uid 查询主播信息';
  schema = z.object({
    roomid: z.string().describe('直播间id')
  });
  async _call({ roomid }) {
    const res = await axios.post(...);
    return formatForAI(res);
  }
}

Advantages:

Callable by agents.

Typed schema for parameters.

Some modularity.

Limitations:

Lifecycle tied to the Flowise project.

Requires TypeScript and a Node project, hurting developer efficiency.

Not cross‑platform.

JSON handling still manual.

Cannot inject common internal capabilities (e.g., internal SDKs).

These drawbacks motivated a platform‑level solution.

Second Version – Online JS Cloud‑Function Platform (NodeVM Runtime)

We built a sandboxed online JS cloud‑function platform where developers write a plain JS function without needing to know Flowise, LangChain, or Node project setup.

NodeVM Sandbox

Using vm2 's NodeVM, we created a controlled execution environment that injects a set of built‑in capabilities:

File‑system access disabled.

Restricted require to a whitelist.

Injected $yuumi for internal gRPC/HTTP calls.

Injected $json2MarkdownTable to convert JSON to AI‑friendly Markdown tables.

Injected session context variables such as $cookie and $flow.

import { NodeVM } from 'vm2';
import { StructuredTool } from '@langchain/core/tools';

class DynamicTool extends StructuredTool {
  constructor({ name, description, schema, code }) {
    super({ name, description, schema });
    this.code = code; // user‑written JS
  }
  async _call(args, runManager, flowContext) {
    const sandbox = {
      ...Object.fromEntries(Object.entries(args).map(([k, v]) => [`$${k}`, v])),
      $flow: flowContext,
      $cookie: flowContext.cookie,
      $yuumi,
      $json2MarkdownTable,
      $biliLLM: biliLLMClient
    };
    const vm = new NodeVM({ sandbox, console: 'inherit', require: { builtin: allowedBuiltinDeps, external: allowedExternalDeps } });
    return await vm.run(`module.exports = async () => { ${this.code} }()`, __dirname);
  }
}

Online Debugging with Monaco‑Editor

The editor provides syntax highlighting, basic IntelliSense, and a mock panel where developers can supply JSON arguments and see console logs, return values, and errors directly in the sandbox.

Tool Arguments Configuration

We added a visual UI that converts a JSON schema into both a LangChain zod schema and an MCP Tool Arguments schema, generating:

MCP JSON schema.

StructuredTool Zod schema.

NodeVM variable bindings (e.g., $roomid).

This configuration is reusable across the entire platform.

Tool Marketplace

Internal teams can publish their tools to a shared marketplace; other teams can discover and reuse them, turning project‑specific scripts into enterprise‑wide capabilities.

Third Version – MCP Integration (Cross‑Platform Tool Calls)

The Model Context Protocol (MCP) acts as a unified plug‑in board for "large model + tool" interactions. By exposing our tools via MCP we achieve:

Uniform, describable, streamable invocation.

Support for tools running on any host (local, remote, or other teams).

Common Misconception

Simply wrapping an existing HTTP endpoint as an MCP tool is insufficient because:

Natural‑language queries must be mapped to concrete parameters (requires prompts, few‑shot examples, or classification models).

Raw JSON responses are not AI‑friendly; we must convert them to readable text/Markdown.

Proxy Layer and Session Management

We implemented a thin MCP gateway on top of Express:

export function createMCPServer({ app, AppDataSource }) {
  // Single function → MCP‑StreamableHTTP
  app.post('/api/mcp/function-tool/:toolId', singleToolCreateStreamableHTTPServer);
  // Multiple functions → MCP‑StreamableHTTP
  app.post('/api/mcp/:mcpId', multipleToolCreateStreamableHTTPServer);
  // Session reuse
  app.get('/api/mcp/function-tool/:id', handleSessionRequest);
  app.delete('/api/mcp/function-tool/:id', handleSessionRequest);
  app.get('/api/mcp/:mcpId', handleSessionRequest);
  app.delete('/api/mcp/:mcpId', handleSessionRequest);
}

Each request creates or reuses a StreamableHTTPServerTransport identified by a mcp-session-id. The transport pools are cleaned up when the connection closes.

async function createStreamableHTTP({ req, res, username }) {
  const sessionId = req.headers['mcp-session-id'];
  let transport;
  if (sessionId && transports.streamable[sessionId]) {
    transport = transports.streamable[sessionId]; // reuse
  } else if (!sessionId && isInitializeRequest(req.body)) {
    transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: () => randomUUID(),
      onsessioninitialized: (sessionId) => { transports.streamable[sessionId] = transport; }
    });
    transport.onclose = () => { delete transports.streamable[transport.sessionId]; };
    const server = buildMcpServer({ ...config, transport: 'streamable-http' });
    await server.connect(transport);
  } else {
    res.status(400).json({ jsonrpc: '2.0', error: { code: -32000, message: 'Bad Request: No valid session ID provided' }, id: null });
    return;
  }
  await transport.handleRequest(req, res, req.body);
}

Registering Tools with MCP

We convert stored tool metadata (name, description, JSON schema, JS code) into a dynamic StructuredTool, then register it with the official MCP server:

function buildMcpServer({ name, tools, cookie, env, id, type, username, transport }) {
  const server = new McpServer({ name, version: '1.0.0' });
  for (const tool of tools) {
    const obj = {
      name: tool.name,
      description: tool.description,
      schema: z.object(convertSchemaToZod(tool.schema)),
      code: tool.func
    };
    const dynamicTool = new DynamicStructuredTool(obj);
    dynamicTool.setFlowObject({ cookie, env: typeof env === 'object' && Object.keys(env).length ? env : {} });
    const paramsSchema = parseToolArgumentsSchema(tool.schema);
    server.tool(tool.name, tool.description, paramsSchema, async (args, _extra) => {
      const result = await dynamicTool.call(args);
      if (process.env.DEPLOY_ENV === 'prod') reportMcpUse({ id, type, name, username, transport });
      return { content: [{ type: 'text', text: result }] };
    });
  }
  return server;
}

The full execution chain becomes:

MCP → McpServer.tool → DynamicStructuredTool → NodeVM → internal services

.

Security and Identity Verification

MCP is exposed as a cross‑platform service entry point, so we enforce:

Registration requires a signed sign parameter generated according to internal standards.

Proxy layer validates the signature, injects the appropriate $cookie and session context, then forwards the request to internal business APIs.

This design keeps the external interface uniform while keeping the actual business logic safely inside the internal network.

Conclusion

By progressing from Flowise StructuredTool → online JS cloud‑function platform → MCP integration, we achieved a unified tool definition, execution runtime, call protocol, marketplace, and security model. Developers now only need to write a simple JavaScript function to give an AI a new capability, eliminating repetitive integration work and paving the way for further performance and correctness improvements.

AI toolsMCPTool IntegrationLangChainNodeVM
Bilibili Tech
Written by

Bilibili Tech

Provides introductions and tutorials on Bilibili-related technologies.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.