├── .env.example ├── .gitignore ├── tsconfig.json ├── src ├── utils.ts └── index.ts ├── package.json ├── LICENSE └── README.md /.env.example: -------------------------------------------------------------------------------- 1 | LANGFUSE_SECRET_KEY=sk-lf-... 2 | LANGFUSE_PUBLIC_KEY=pk-lf-... 3 | LANGFUSE_BASEURL=https://cloud.langfuse.com -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | 4 | # Build output 5 | dist/ 6 | build/ 7 | 8 | # Environment variables 9 | .env 10 | 11 | # IDE files 12 | .vscode/ 13 | .idea/ 14 | 15 | # Logs 16 | *.log 17 | 18 | # OS files 19 | .DS_Store 20 | Thumbs.db -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./build", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true 12 | }, 13 | "include": ["src/**/*"], 14 | "exclude": ["node_modules"] 15 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | // Regex for valid variable names (letters, underscores, starting with letter) 2 | export const VARIABLE_REGEX = /^[a-zA-Z][a-zA-Z_]*$/; 3 | 4 | // Regex to find variables in mustache syntax 5 | export const MUSTACHE_REGEX = /{{([^{}]*)}}+/g; 6 | 7 | // Regex to find multiline variables 8 | export const MULTILINE_VARIABLE_REGEX = /{{[^}]*\n[^}]*}}/g; 9 | 10 | // Regex to find unclosed variables 11 | export const UNCLOSED_VARIABLE_REGEX = /{{(?![^{]*}})/g; 12 | 13 | export function isValidVariableName(variable: string): boolean { 14 | return VARIABLE_REGEX.test(variable); 15 | } 16 | 17 | export function extractVariables(mustacheString: string): string[] { 18 | const matches = Array.from(mustacheString.matchAll(MUSTACHE_REGEX)) 19 | .map((match) => match[1]) 20 | .filter(isValidVariableName); 21 | return [...new Set(matches)]; 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-server-langfuse", 3 | "version": "0.0.1", 4 | "description": "A MCP Server for Langfuse Prompt Management", 5 | "main": "index.js", 6 | "type": "module", 7 | "bin": { 8 | "mcp-server-langfuse": "./build/index.js" 9 | }, 10 | "scripts": { 11 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"" 12 | }, 13 | "files": [ 14 | "build" 15 | ], 16 | "dependencies": { 17 | "@modelcontextprotocol/sdk": "^1.5.0", 18 | "langfuse": "^3.35.2", 19 | "zod": "^3.24.2" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^22.13.4", 23 | "typescript": "^5.7.3" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "hu" 28 | }, 29 | "keywords": [ 30 | "langfuse", 31 | "model-context-protocol", 32 | "prompt", 33 | "management" 34 | ], 35 | "author": "marcklingen", 36 | "license": "MIT" 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Langfuse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Langfuse Prompt Management MCP Server 2 | 3 | [Model Context Protocol](https://github.com/modelcontextprotocol) (MCP) Server for [Langfuse Prompt Management](https://langfuse.com/docs/prompts/get-started). This server allows you to access and manage your Langfuse prompts through the Model Context Protocol. 4 | 5 | ## Demo 6 | 7 | Quick demo of Langfuse Prompts MCP in Claude Desktop (_unmute for voice-over explanations_): 8 | 9 | https://github.com/user-attachments/assets/61da79af-07c2-4f69-b28c-ca7c6e606405 10 | 11 | ## Features 12 | 13 | ### MCP Prompt 14 | 15 | This server implements the [MCP Prompts specification](https://modelcontextprotocol.io/docs/concepts/prompts) for prompt discovery and retrieval. 16 | 17 | - `prompts/list`: List all available prompts 18 | 19 | - Optional cursor-based pagination 20 | - Returns prompt names and their required arguments, limitation: all arguments are assumed to be optional and do not include descriptions as variables do not have specification in Langfuse 21 | - Includes next cursor for pagination if there's more than 1 page of prompts 22 | 23 | - `prompts/get`: Get a specific prompt 24 | 25 | - Transforms Langfuse prompts (text and chat) into MCP prompt objects 26 | - Compiles prompt with provided variables 27 | 28 | ### Tools 29 | 30 | To increase compatibility with other MCP clients that do not support the prompt capability, the server also exports tools that replicate the functionality of the MCP Prompts. 31 | 32 | - `get-prompts`: List available prompts 33 | 34 | - Optional `cursor` parameter for pagination 35 | - Returns a list of prompts with their arguments 36 | 37 | - `get-prompt`: Retrieve and compile a specific prompt 38 | - Required `name` parameter: Name of the prompt to retrieve 39 | - Optional `arguments` parameter: JSON object with prompt variables 40 | 41 | ## Development 42 | 43 | ```bash 44 | npm install 45 | 46 | # build current file 47 | npm run build 48 | 49 | # test in mcp inspector 50 | npx @modelcontextprotocol/inspector node ./build/index.js 51 | ``` 52 | 53 | ## Usage 54 | 55 | ### Step 1: Build 56 | 57 | ```bash 58 | npm install 59 | npm run build 60 | ``` 61 | 62 | ### Step 2: Add the server to your MCP servers: 63 | 64 | #### Claude Desktop 65 | 66 | Configure Claude for Desktop by editing `claude_desktop_config.json` 67 | 68 | ```json 69 | { 70 | "mcpServers": { 71 | "langfuse": { 72 | "command": "node", 73 | "args": ["/build/index.js"], 74 | "env": { 75 | "LANGFUSE_PUBLIC_KEY": "your-public-key", 76 | "LANGFUSE_SECRET_KEY": "your-secret-key", 77 | "LANGFUSE_BASEURL": "https://cloud.langfuse.com" 78 | } 79 | } 80 | } 81 | } 82 | ``` 83 | 84 | Make sure to replace the environment variables with your actual Langfuse API keys. The server will now be available to use in Claude Desktop. 85 | 86 | #### Cursor 87 | 88 | Add new server to Cursor: 89 | 90 | - Name: `Langfuse Prompts` 91 | - Type: `command` 92 | - Command: 93 | ```bash 94 | LANGFUSE_PUBLIC_KEY="your-public-key" LANGFUSE_SECRET_KEY="your-secret-key" LANGFUSE_BASEURL="https://cloud.langfuse.com" node absolute-path/build/index.js 95 | ``` 96 | 97 | ## Limitations 98 | 99 | The MCP Server is a work in progress and has some limitations: 100 | 101 | - Only prompts with a `production` label in Langfuse are returned 102 | - All arguments are assumed to be optional and do not include descriptions as variables do not have specification in Langfuse 103 | - List operations require fetching each prompt individually in the background to extract the arguments, this works but is not efficient 104 | 105 | Contributions are welcome! Please open an issue or a PR ([repo](https://github.com/langfuse/mcp-server-langfuse)) if you have any suggestions or feedback. 106 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 3 | import { 4 | ListPromptsRequestSchema, 5 | ListPromptsRequest, 6 | ListPromptsResult, 7 | GetPromptRequestSchema, 8 | GetPromptRequest, 9 | GetPromptResult, 10 | CallToolResult, 11 | } from "@modelcontextprotocol/sdk/types.js"; 12 | import { Langfuse, ChatPromptClient } from "langfuse"; 13 | import { extractVariables } from "./utils.js"; 14 | import { z } from "zod"; 15 | 16 | // Requires Environment Variables 17 | const langfuse = new Langfuse(); 18 | 19 | // Create MCP server instance with a "prompts" capability. 20 | const server = new McpServer( 21 | { 22 | name: "langfuse-prompts", 23 | version: "1.0.0", 24 | }, 25 | { 26 | capabilities: { 27 | prompts: {}, 28 | }, 29 | } 30 | ); 31 | 32 | async function listPromptsHandler( 33 | request: ListPromptsRequest 34 | ): Promise { 35 | try { 36 | const cursor = request.params?.cursor; 37 | const page = cursor ? Number(cursor) : 1; 38 | if (cursor !== undefined && isNaN(page)) { 39 | throw new Error("Cursor must be a valid number"); 40 | } 41 | 42 | const res = await langfuse.api.promptsList({ 43 | limit: 100, 44 | page, 45 | label: "production", 46 | }); 47 | 48 | const resPrompts: ListPromptsResult["prompts"] = await Promise.all( 49 | res.data.map(async (i) => { 50 | const prompt = await langfuse.getPrompt(i.name, undefined, { 51 | cacheTtlSeconds: 0, 52 | }); 53 | const variables = extractVariables(JSON.stringify(prompt.prompt)); 54 | return { 55 | name: i.name, 56 | arguments: variables.map((v) => ({ 57 | name: v, 58 | required: false, 59 | })), 60 | }; 61 | }) 62 | ); 63 | 64 | return { 65 | prompts: resPrompts, 66 | nextCursor: 67 | res.meta.totalPages > page ? (page + 1).toString() : undefined, 68 | }; 69 | } catch (error) { 70 | console.error("Error fetching prompts:", error); 71 | throw new Error("Failed to fetch prompts"); 72 | } 73 | } 74 | 75 | async function getPromptHandler( 76 | request: GetPromptRequest 77 | ): Promise { 78 | const promptName: string = request.params.name; 79 | const args = request.params.arguments || {}; 80 | 81 | try { 82 | // Initialize Langfuse client and fetch the prompt by name. 83 | let compiledTextPrompt: string | undefined; 84 | let compiledChatPrompt: ChatPromptClient["prompt"] | undefined; // Langfuse chat prompt type 85 | 86 | try { 87 | // try chat prompt type first 88 | const prompt = await langfuse.getPrompt(promptName, undefined, { 89 | type: "chat", 90 | }); 91 | if (prompt.type !== "chat") { 92 | throw new Error(`Prompt '${promptName}' is not a chat prompt`); 93 | } 94 | compiledChatPrompt = prompt.compile(args); 95 | } catch (error) { 96 | // fallback to text prompt type 97 | const prompt = await langfuse.getPrompt(promptName, undefined, { 98 | type: "text", 99 | }); 100 | compiledTextPrompt = prompt.compile(args); 101 | } 102 | 103 | if (compiledChatPrompt) { 104 | const result: GetPromptResult = { 105 | messages: compiledChatPrompt.map((msg) => ({ 106 | role: ["ai", "assistant"].includes(msg.role) ? "assistant" : "user", 107 | content: { 108 | type: "text", 109 | text: msg.content, 110 | }, 111 | })), 112 | }; 113 | return result; 114 | } else if (compiledTextPrompt) { 115 | const result: GetPromptResult = { 116 | messages: [ 117 | { 118 | role: "user", 119 | content: { type: "text", text: compiledTextPrompt }, 120 | }, 121 | ], 122 | }; 123 | return result; 124 | } else { 125 | throw new Error(`Failed to get prompt for '${promptName}'`); 126 | } 127 | } catch (error: any) { 128 | throw new Error( 129 | `Failed to get prompt for '${promptName}': ${error.message}` 130 | ); 131 | } 132 | } 133 | 134 | // Register handlers 135 | server.server.setRequestHandler(ListPromptsRequestSchema, listPromptsHandler); 136 | server.server.setRequestHandler(GetPromptRequestSchema, getPromptHandler); 137 | 138 | // Tools for compatibility 139 | server.tool( 140 | "get-prompts", 141 | "Get prompts that are stored in Langfuse", 142 | { 143 | cursor: z 144 | .string() 145 | .optional() 146 | .describe("Cursor to paginate through prompts"), 147 | }, 148 | async (args) => { 149 | try { 150 | const res = await listPromptsHandler({ 151 | method: "prompts/list", 152 | params: { 153 | cursor: args.cursor, 154 | }, 155 | }); 156 | 157 | const parsedRes: CallToolResult = { 158 | content: res.prompts.map((p) => ({ 159 | type: "text", 160 | text: JSON.stringify(p), 161 | })), 162 | }; 163 | 164 | return parsedRes; 165 | } catch (error) { 166 | return { 167 | content: [{ type: "text", text: "Error: " + error }], 168 | isError: true, 169 | }; 170 | } 171 | } 172 | ); 173 | 174 | server.tool( 175 | "get-prompt", 176 | "Get a prompt that is stored in Langfuse", 177 | { 178 | name: z 179 | .string() 180 | .describe( 181 | "Name of the prompt to retrieve, use get-prompts to get a list of prompts" 182 | ), 183 | arguments: z 184 | .record(z.string()) 185 | .optional() 186 | .describe( 187 | 'Arguments with prompt variables to pass to the prompt template, json object, e.g. {"":""}' 188 | ), 189 | }, 190 | async (args, extra) => { 191 | try { 192 | const res = await getPromptHandler({ 193 | method: "prompts/get", 194 | params: { 195 | name: args.name, 196 | arguments: args.arguments, 197 | }, 198 | }); 199 | 200 | const parsedRes: CallToolResult = { 201 | content: [ 202 | { 203 | type: "text", 204 | text: JSON.stringify(res), 205 | }, 206 | ], 207 | }; 208 | 209 | return parsedRes; 210 | } catch (error) { 211 | return { 212 | content: [{ type: "text", text: "Error: " + error }], 213 | isError: true, 214 | }; 215 | } 216 | } 217 | ); 218 | 219 | async function main() { 220 | const transport = new StdioServerTransport(); 221 | await server.connect(transport); 222 | console.error("Langfuse Prompts MCP Server running on stdio"); 223 | } 224 | 225 | main().catch((error) => { 226 | console.error("Fatal error in main():", error); 227 | process.exit(1); 228 | }); 229 | --------------------------------------------------------------------------------