diff --git a/src/lib/tools/hello.ts b/src/lib/tools/hello.ts new file mode 100644 index 0000000..9c527c8 --- /dev/null +++ b/src/lib/tools/hello.ts @@ -0,0 +1,34 @@ +import type { ToolResult } from "./types.js"; + +export interface HelloOutput { + message: string; + serverName: string; + serverVersion: string; + vaultId: string; + anonymousMode: boolean; + timestamp: string; +} + +export interface HelloContext { + serverName: string; + serverVersion: string; + vaultId: string; + anonymousMode: boolean; +} + +export async function handleHello( + name: string | undefined, + context: HelloContext +): Promise> { + const greeting = name?.trim() ? `Hello, ${name.trim()}!` : "Hello from Skyflow Runtime MCP!"; + return { + output: { + message: greeting, + serverName: context.serverName, + serverVersion: context.serverVersion, + vaultId: context.vaultId, + anonymousMode: context.anonymousMode, + timestamp: new Date().toISOString(), + }, + }; +} diff --git a/src/server.ts b/src/server.ts index 81923ed..0e111d1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -18,6 +18,7 @@ import { validateVaultConfig, looksLikePlaceholder } from "./lib/validation/vaul import { ENTITY_KEYS } from "./lib/mappings/entityMaps.js"; import { handleDeIdentify } from "./lib/tools/deIdentify.js"; import { handleReIdentify } from "./lib/tools/reIdentify.js"; +import { handleHello } from "./lib/tools/hello.js"; import { toStructuredContent } from "./lib/tools/types.js"; import { authenticateBearer } from "./lib/middleware/authenticateBearer.js"; import { @@ -59,10 +60,20 @@ function isAnonymousMode(): boolean { return context.isAnonymousMode; } +function getCurrentVaultId(): string { + const context = requestContextStorage.getStore(); + if (!context) { + throw new Error("No request context available"); + } + return context.vaultId; +} + // Create an MCP server +const SERVER_NAME = "Skyflow Runtime MCP Server"; +const SERVER_VERSION = "0.4.0"; const server = new McpServer({ - name: "Skyflow Runtime MCP Server", - version: "0.4.0", + name: SERVER_NAME, + version: SERVER_VERSION, }); // MCP Apps: Resource URIs @@ -168,6 +179,43 @@ registerAppTool( } ); +/** + * Hello / self tool + * Echoes back basic metadata about the server and the caller's connection. + * Useful as a health check and to confirm credentials/vault wiring. + */ +server.registerTool( + "hello", + { + title: "Hello / Self", + description: + "Health check / self endpoint. Returns server name, version, the vault ID this connection is using, whether the session is in anonymous mode, and a timestamp. Pass an optional name to get a personalized greeting.", + inputSchema: { + name: z.string().optional().describe("Optional name to include in the greeting"), + }, + outputSchema: { + message: z.string(), + serverName: z.string(), + serverVersion: z.string(), + vaultId: z.string(), + anonymousMode: z.boolean(), + timestamp: z.string().describe("ISO-8601 timestamp when the response was generated"), + }, + }, + async ({ name }) => { + const result = await handleHello(name, { + serverName: SERVER_NAME, + serverVersion: SERVER_VERSION, + vaultId: getCurrentVaultId(), + anonymousMode: isAnonymousMode(), + }); + return { + content: [{ type: "text", text: JSON.stringify(result.output) }], + structuredContent: toStructuredContent(result.output), + }; + } +); + const app: Express = express(); app.use(express.json({ limit: "5mb" })); // Limit for base64-encoded files diff --git a/tests/unit/tools/hello.test.ts b/tests/unit/tools/hello.test.ts new file mode 100644 index 0000000..9280499 --- /dev/null +++ b/tests/unit/tools/hello.test.ts @@ -0,0 +1,45 @@ +import { describe, it, expect } from "vitest"; +import { handleHello } from "../../../src/lib/tools/hello"; + +const baseContext = { + serverName: "Test Server", + serverVersion: "9.9.9", + vaultId: "vault_abc", + anonymousMode: false, +}; + +describe("handleHello", () => { + it("returns a default greeting when no name is provided", async () => { + const result = await handleHello(undefined, baseContext); + expect(result.isError).toBeUndefined(); + expect(result.output.message).toBe("Hello from Skyflow Runtime MCP!"); + }); + + it("personalizes the greeting when a name is provided", async () => { + const result = await handleHello("Joe", baseContext); + expect(result.output.message).toBe("Hello, Joe!"); + }); + + it("treats a blank name as no name", async () => { + const result = await handleHello(" ", baseContext); + expect(result.output.message).toBe("Hello from Skyflow Runtime MCP!"); + }); + + it("echoes connection metadata", async () => { + const result = await handleHello(undefined, { + ...baseContext, + anonymousMode: true, + vaultId: "vault_xyz", + }); + expect(result.output.serverName).toBe("Test Server"); + expect(result.output.serverVersion).toBe("9.9.9"); + expect(result.output.vaultId).toBe("vault_xyz"); + expect(result.output.anonymousMode).toBe(true); + }); + + it("returns an ISO-8601 timestamp", async () => { + const result = await handleHello(undefined, baseContext); + expect(() => new Date(result.output.timestamp).toISOString()).not.toThrow(); + expect(result.output.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T/); + }); +});