Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  mcp-http.test.ts

  Sprache: JAVA
 

Spracherkennung für: .ts vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { getFreePortBlockWithPermissionFallback } from "../test-utils/ports.js";

type MockGatewayTool = {
  name: string;
  description: string;
  parameters: Record<string, unknown>;
  ownerOnly?: boolean;
  execute: (...args: unknown[]) => Promise<{ content: Array<{ type: string; text: string }> }>;
};

type MockGatewayScopedTools = {
  agentId: string;
  tools: MockGatewayTool[];
};

type MockBeforeToolCallHookResult =
  | { blocked: true; reason: string }
  | { blocked: false; params: unknown };

const runBeforeToolCallHookMock = vi.hoisted(() =>
  vi.fn(
    async (args: { params: unknown }): Promise<MockBeforeToolCallHookResult> => ({
      blocked: false,
      params: args.params,
    }),
  ),
);

const resolveGatewayScopedToolsMock = vi.hoisted(() =>
  vi.fn<(...args: unknown[]) => MockGatewayScopedTools>(() => ({
    agentId: "main",
    tools: [
      {
        name: "message",
        description: "send a message",
        parameters: { type: "object", properties: {} },
        execute: async () => ({
          content: [{ type: "text", text: "ok" }],
        }),
      },
    ],
  })),
);

vi.mock("../config/config.js", () => ({
  loadConfig: () => ({ session: { mainKey: "main" } }),
}));

vi.mock("../config/sessions.js", () => ({
  resolveMainSessionKey: () => "agent:main:main",
}));

vi.mock("../agents/pi-tools.before-tool-call.js", () => ({
  runBeforeToolCallHook: (...args: Parameters<typeof runBeforeToolCallHookMock>) =>
    runBeforeToolCallHookMock(...args),
}));

vi.mock("./tool-resolution.js", () => ({
  resolveGatewayScopedTools: (...args: Parameters<typeof resolveGatewayScopedToolsMock>) =>
    resolveGatewayScopedToolsMock(...args),
}));

import {
  createMcpLoopbackServerConfig,
  closeMcpLoopbackServer,
  getActiveMcpLoopbackRuntime,
  resolveMcpLoopbackBearerToken,
  ensureMcpLoopbackServer,
  startMcpLoopbackServer,
} from "./mcp-http.js";

let server: Awaited<ReturnType<typeof startMcpLoopbackServer>> | undefined;

async function sendRaw(params: {
  port: number;
  token?: string;
  headers?: Record<string, string>;
  body?: string;
}) {
  return await fetch(`http://127.0.0.1:${params.port}/mcp`, {
    method: "POST",
    headers: {
      ...(params.token ? { authorization: `Bearer ${params.token}` } : {}),
      ...params.headers,
    },
    body: params.body,
  });
}

beforeEach(() => {
  resolveGatewayScopedToolsMock.mockClear();
  runBeforeToolCallHookMock.mockClear();
  runBeforeToolCallHookMock.mockImplementation(
    async (args: { params: unknown }): Promise<MockBeforeToolCallHookResult> => ({
      blocked: false,
      params: args.params,
    }),
  );
  resolveGatewayScopedToolsMock.mockReturnValue({
    agentId: "main",
    tools: [
      {
        name: "message",
        description: "send a message",
        parameters: { type: "object", properties: {} },
        execute: async () => ({
          content: [{ type: "text", text: "ok" }],
        }),
      },
    ],
  });
});

afterEach(async () => {
  await server?.close();
  server = undefined;
});

describe("mcp loopback server", () => {
  it("passes session, account, and message channel headers into shared tool resolution", async () => {
    const port = await getFreePortBlockWithPermissionFallback({
      offsets: [0],
      fallbackBase: 53_000,
    });
    server = await startMcpLoopbackServer(port);
    const runtime = getActiveMcpLoopbackRuntime();

    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        "x-session-key": "agent:main:telegram:group:chat123",
        "x-openclaw-account-id": "work",
        "x-openclaw-message-channel": "telegram",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });

    expect(response.status).toBe(200);
    expect(resolveGatewayScopedToolsMock).toHaveBeenCalledWith(
      expect.objectContaining({
        sessionKey: "agent:main:telegram:group:chat123",
        accountId: "work",
        messageProvider: "telegram",
        senderIsOwner: false,
        surface: "loopback",
      }),
    );
  });

  it("adds empty properties for object schemas that omit properties", async () => {
    resolveGatewayScopedToolsMock.mockReturnValue({
      agentId: "main",
      tools: [
        {
          name: "schema_probe",
          description: "exercise no-argument MCP schemas",
          parameters: { type: "object" },
          execute: async () => ({
            content: [{ type: "text", text: "ok" }],
          }),
        },
      ],
    });
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();

    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        "x-session-key": "agent:main:main",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });
    const payload = (await response.json()) as {
      result?: { tools?: Array<{ inputSchema?: Record<string, unknown> }> };
    };

    expect(response.status).toBe(200);
    expect(payload.result?.tools?.[0]?.inputSchema).toEqual({
      type: "object",
      properties: {},
    });
  });

  it("derives senderIsOwner from the loopback bearer token", async () => {
    server = await startMcpLoopbackServer(0);
    const activeServer = server;
    const runtime = getActiveMcpLoopbackRuntime();

    const sendToolsList = async (senderIsOwner: "true" | "false") =>
      await sendRaw({
        port: activeServer.port,
        token: runtime
          ? resolveMcpLoopbackBearerToken(runtime, senderIsOwner === "true")
          : undefined,
        headers: {
          "content-type": "application/json",
          "x-session-key": "agent:main:matrix:dm:test",
          "x-openclaw-message-channel": "matrix",
        },
        body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
      });

    expect((await sendToolsList("true")).status).toBe(200);
    expect((await sendToolsList("false")).status).toBe(200);

    expect(resolveGatewayScopedToolsMock).toHaveBeenCalledTimes(2);
    expect(resolveGatewayScopedToolsMock).toHaveBeenNthCalledWith(
      1,
      expect.objectContaining({
        sessionKey: "agent:main:matrix:dm:test",
        messageProvider: "matrix",
        senderIsOwner: true,
        surface: "loopback",
      }),
    );
    expect(resolveGatewayScopedToolsMock).toHaveBeenNthCalledWith(
      2,
      expect.objectContaining({
        sessionKey: "agent:main:matrix:dm:test",
        messageProvider: "matrix",
        senderIsOwner: false,
        surface: "loopback",
      }),
    );
  });

  it("ignores spoofed owner headers when the bearer token is non-owner scoped", async () => {
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();

    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        "x-session-key": "agent:main:matrix:dm:test",
        "x-openclaw-message-channel": "matrix",
        "x-openclaw-sender-is-owner": "true",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });

    expect(response.status).toBe(200);
    expect(resolveGatewayScopedToolsMock).toHaveBeenCalledWith(
      expect.objectContaining({
        sessionKey: "agent:main:matrix:dm:test",
        messageProvider: "matrix",
        senderIsOwner: false,
        surface: "loopback",
      }),
    );
  });

  it("filters owner-only tools from non-owner tool lists", async () => {
    resolveGatewayScopedToolsMock.mockReturnValue({
      agentId: "main",
      tools: [
        {
          name: "message",
          description: "send a message",
          parameters: { type: "object", properties: {} },
          execute: async () => ({
            content: [{ type: "text", text: "ok" }],
          }),
        },
        {
          name: "cron",
          description: "manage schedules",
          parameters: { type: "object", properties: {} },
          execute: async () => ({
            content: [{ type: "text", text: "cron" }],
          }),
        },
        {
          name: "owner_probe",
          description: "owner-only by flag",
          parameters: { type: "object", properties: {} },
          ownerOnly: true,
          execute: async () => ({
            content: [{ type: "text", text: "owner" }],
          }),
        },
      ],
    });
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();

    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        "x-session-key": "agent:main:main",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });
    const payload = (await response.json()) as {
      result?: { tools?: Array<{ name: string }> };
    };
    const names = (payload.result?.tools ?? []).map((tool) => tool.name);

    expect(response.status).toBe(200);
    expect(names).toContain("message");
    expect(names).not.toContain("cron");
    expect(names).not.toContain("owner_probe");
  });

  it("keeps owner-only tools available to owner loopback callers", async () => {
    resolveGatewayScopedToolsMock.mockReturnValue({
      agentId: "main",
      tools: [
        {
          name: "message",
          description: "send a message",
          parameters: { type: "object", properties: {} },
          execute: async () => ({
            content: [{ type: "text", text: "ok" }],
          }),
        },
        {
          name: "cron",
          description: "manage schedules",
          parameters: { type: "object", properties: {} },
          execute: async () => ({
            content: [{ type: "text", text: "cron" }],
          }),
        },
      ],
    });
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();

    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, true) : undefined,
      headers: {
        "content-type": "application/json",
        "x-session-key": "agent:main:main",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });
    const payload = (await response.json()) as {
      result?: { tools?: Array<{ name: string }> };
    };
    const names = (payload.result?.tools ?? []).map((tool) => tool.name);

    expect(response.status).toBe(200);
    expect(names).toContain("message");
    expect(names).toContain("cron");
  });

  it("does not execute owner-only tools for non-owner callers", async () => {
    const cronExecute = vi.fn(async () => ({
      content: [{ type: "text", text: "CRON_EXECUTED" }],
    }));
    resolveGatewayScopedToolsMock.mockReturnValue({
      agentId: "main",
      tools: [
        {
          name: "message",
          description: "send a message",
          parameters: { type: "object", properties: {} },
          execute: async () => ({
            content: [{ type: "text", text: "ok" }],
          }),
        },
        {
          name: "cron",
          description: "manage schedules",
          parameters: { type: "object", properties: {} },
          execute: cronExecute,
        },
      ],
    });
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();

    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        "x-session-key": "agent:main:main",
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "cron", arguments: {} },
      }),
    });
    const payload = (await response.json()) as {
      result?: { content?: Array<{ text?: string }>; isError?: boolean };
    };

    expect(response.status).toBe(200);
    expect(cronExecute).not.toHaveBeenCalled();
    expect(payload.result?.isError).toBe(true);
    expect(payload.result?.content?.[0]?.text).toBe("Tool not available: cron");
  });

  it("honors before-tool-call hook blocks before loopback tool execution", async () => {
    const execute = vi.fn(async () => ({
      content: [{ type: "text", text: "EXECUTED" }],
    }));
    runBeforeToolCallHookMock.mockResolvedValueOnce({
      blocked: true,
      reason: "blocked by hook",
    });
    resolveGatewayScopedToolsMock.mockReturnValue({
      agentId: "main",
      tools: [
        {
          name: "message",
          description: "send a message",
          parameters: { type: "object", properties: {} },
          execute,
        },
      ],
    });
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();

    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        "x-session-key": "agent:main:main",
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "message", arguments: { body: "hello" } },
      }),
    });
    const payload = (await response.json()) as {
      result?: { content?: Array<{ text?: string }>; isError?: boolean };
    };

    expect(response.status).toBe(200);
    expect(runBeforeToolCallHookMock).toHaveBeenCalledWith(
      expect.objectContaining({
        toolName: "message",
        params: { body: "hello" },
        ctx: expect.objectContaining({
          agentId: "main",
          sessionKey: "agent:main:main",
        }),
        signal: expect.any(AbortSignal),
      }),
    );
    expect(execute).not.toHaveBeenCalled();
    expect(payload.result?.isError).toBe(true);
    expect(payload.result?.content?.[0]?.text).toBe("blocked by hook");
  });

  it("forwards the request abort signal to loopback tool execution", async () => {
    const execute = vi.fn(async () => ({
      content: [{ type: "text", text: "EXECUTED" }],
    }));
    resolveGatewayScopedToolsMock.mockReturnValue({
      agentId: "main",
      tools: [
        {
          name: "message",
          description: "send a message",
          parameters: { type: "object", properties: {} },
          execute,
        },
      ],
    });
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();

    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        "x-session-key": "agent:main:main",
      },
      body: JSON.stringify({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "message", arguments: { body: "hello" } },
      }),
    });
    const payload = (await response.json()) as {
      result?: { isError?: boolean };
    };

    expect(response.status).toBe(200);
    expect(payload.result?.isError).toBe(false);
    expect(execute).toHaveBeenCalledWith(
      expect.stringMatching(/^mcp-/),
      { body: "hello" },
      expect.any(AbortSignal),
    );
  });

  it("tracks the active runtime only while the server is running", async () => {
    server = await startMcpLoopbackServer(0);
    const active = getActiveMcpLoopbackRuntime();
    expect(active?.port).toBe(server.port);
    expect(active?.ownerToken).toMatch(/^[0-9a-f]{64}$/);
    expect(active?.nonOwnerToken).toMatch(/^[0-9a-f]{64}$/);

    await server.close();
    server = undefined;
    expect(getActiveMcpLoopbackRuntime()).toBeUndefined();
  });

  it("starts the loopback server lazily and reuses the same singleton", async () => {
    expect(getActiveMcpLoopbackRuntime()).toBeUndefined();

    const first = await ensureMcpLoopbackServer(0);
    const second = await ensureMcpLoopbackServer(0);

    expect(second).toBe(first);
    expect(getActiveMcpLoopbackRuntime()?.port).toBe(first.port);

    await closeMcpLoopbackServer();
    expect(getActiveMcpLoopbackRuntime()).toBeUndefined();
  });

  it("returns 401 when the bearer token is missing", async () => {
    server = await startMcpLoopbackServer(0);
    const response = await sendRaw({
      port: server.port,
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });
    expect(response.status).toBe(401);
  });

  it("returns 415 when the content type is not JSON", async () => {
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();
    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: { "content-type": "text/plain" },
      body: "{}",
    });
    expect(response.status).toBe(415);
  });

  it("rejects cross-origin browser requests before auth", async () => {
    server = await startMcpLoopbackServer(0);
    const response = await sendRaw({
      port: server.port,
      headers: {
        "content-type": "application/json",
        origin: "https://evil.example",
        "sec-fetch-site": "cross-site",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });

    expect(response.status).toBe(403);
  });

  it("rejects non-loopback origins even without fetch metadata", async () => {
    server = await startMcpLoopbackServer(0);
    const response = await sendRaw({
      port: server.port,
      headers: {
        "content-type": "application/json",
        origin: "https://evil.example",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });

    expect(response.status).toBe(403);
  });

  it("allows loopback browser origins for local clients", async () => {
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();
    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        origin: "http://127.0.0.1:43123",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });

    expect(response.status).toBe(200);
  });

  it("allows same-origin browser requests from loopback clients", async () => {
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();
    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        origin: `http://127.0.0.1:${server.port}`,
        "sec-fetch-site": "same-origin",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });

    expect(response.status).toBe(200);
  });

  it("allows cross-site fetch metadata when both ends are loopback (localhost ↔ 127.0.0.1)", async () => {
    // Browsers report a request from a `http://localhost:<ui-port>`
    // page to `http://127.0.0.1:<mcp-port>` as Sec-Fetch-Site:
    // cross-site even though both ends are loopback. The gate must
    // not blanket-reject on the cross-site signal — checkBrowserOrigin
    // already authorizes loopback origins from loopback peers via
    // its `local-loopback` matcher.
    server = await startMcpLoopbackServer(0);
    const runtime = getActiveMcpLoopbackRuntime();
    const response = await sendRaw({
      port: server.port,
      token: runtime ? resolveMcpLoopbackBearerToken(runtime, false) : undefined,
      headers: {
        "content-type": "application/json",
        origin: "http://localhost:43123",
        "sec-fetch-site": "cross-site",
      },
      body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list" }),
    });

    expect(response.status).toBe(200);
  });
});

describe("createMcpLoopbackServerConfig", () => {
  it("builds a server entry with env-driven headers", () => {
    const config = createMcpLoopbackServerConfig(23119) as {
      mcpServers?: Record<string, { url?: string; headers?: Record<string, string> }>;
    };
    expect(config.mcpServers?.openclaw?.url).toBe("http://127.0.0.1:23119/mcp");
    expect(config.mcpServers?.openclaw?.headers?.Authorization).toBe(
      "Bearer ${OPENCLAW_MCP_TOKEN}",
    );
    expect(config.mcpServers?.openclaw?.headers?.["x-openclaw-message-channel"]).toBe(
      "${OPENCLAW_MCP_MESSAGE_CHANNEL}",
    );
    expect(config.mcpServers?.openclaw?.headers?.["x-openclaw-sender-is-owner"]).toBeUndefined();
  });
});

¤ Dauer der Verarbeitung: 0.2 Sekunden  (vorverarbeitet am  2026-04-27) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge