# Directory Structure ``` ├── .env.example ├── .github │ └── workflows │ └── issue-solver.yml ├── .gitignore ├── Dockerfile ├── package-lock.json ├── package.json ├── README.md ├── smithery.yaml ├── src │ └── index.ts └── tsconfig.json ``` # Files -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` # AgentOps API Key - Required for authentication AGENTOPS_API_KEY="your-agentops-api-key-here" # Optional: Override the default API host (defaults to https://api.agentops.ai) # HOST="https://api.agentops.ai" ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Node.js node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* # TypeScript *.tsbuildinfo # Coverage directory used by tools like istanbul coverage/ *.lcov # Dependency directories node_modules/ jspm_packages/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # Environments .env .envrc .env.local .env.development.local .env.test.local .env.production.local # Runtime data pids *.pid *.seed *.pid.lock # Logs logs *.log # OS generated files .DS_Store .DS_Store? ._* .Spotlight-V100 .Trashes ehthumbs.db Thumbs.db # IDE files .vscode/ .idea/ *.swp *.swo *~ # Temporary files *.tmp *.temp # Build artifacts build/ dist/ tmp/ # Package files *.tar.gz *.zip ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # AgentOps MCP Server [](https://smithery.ai/server/@AgentOps-AI/agentops-mcp) The AgentOps MCP server provides access to observability and tracing data for debugging complex AI agent runs. This adds crucial context about where the AI agent succeeds or fails. ## Usage ### MCP Client Configuration Add the following to your MCP configuration file: ```json { "mcpServers": { "agentops-mcp": { "command": "npx", "args": ["agentops-mcp"], "env": { "AGENTOPS_API_KEY": "" } } } } ``` ## Installation ### Installing via Cursor Deeplink [](https://cursor.com/install-mcp?name=agentops&config=eyJjb21tYW5kIjoibnB4IGFnZW50b3BzLW1jcCIsImVudiI6eyJBR0VOVE9QU19BUElfS0VZIjoiIn19) ### Installing via Smithery To install agentops-mcp for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@AgentOps-AI/agentops-mcp): ```bash npx -y @smithery/cli install @AgentOps-AI/agentops-mcp --client claude ``` ### Local Development To build the MCP server locally: ```bash # Clone and setup git clone https://github.com/AgentOps-AI/agentops-mcp.git cd mcp npm install # Build the project npm run build # Run the server npm pack ``` ## Available Tools ### `auth` Authorize using an AgentOps project API key and return JWT token. **Parameters:** - `api_key` (string): Your AgentOps project API key ### `get_trace` Retrieve trace information by ID. **Parameters:** - `trace_id` (string): The trace ID to retrieve ### `get_span` Get span information by ID. **Parameters:** - `span_id` (string): The span ID to retrieve ### `get_complete_trace` Get comprehensive trace information including all spans and their metrics. **Parameters:** - `trace_id` (string): The trace ID ## Requirements - Node.js >= 18.0.0 - AgentOps API key (passed as parameter to tools) ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config # Dockerfile for AgentOps MCP Server FROM node:lts-alpine AS builder WORKDIR /app # Install dependencies and build COPY package.json package-lock.json tsconfig.json ./ COPY src ./src RUN npm install --ignore-scripts && npm run build # Production image FROM node:lts-alpine WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules COPY README.md ./README.md ENV NODE_ENV=production ENTRYPOINT ["node", "dist/server.js"] ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml # Smithery configuration file: https://smithery.ai/docs/build/project-config startCommand: type: stdio commandFunction: # A JS function that produces the CLI command based on the given config to start the MCP on stdio. |- (config) => ({ command: 'node', args: ['dist/index.js'], env: config.apiKey ? { AGENTOPS_API_KEY: config.apiKey } : {} }) configSchema: # JSON Schema defining the configuration options for the MCP. type: object required: [] properties: apiKey: type: string description: AgentOps project API key (optional) exampleConfig: {} ``` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- ```json { "name": "agentops-mcp", "version": "0.3.5", "description": "MCP index access to the AgentOps Public API.", "main": "dist/index.js", "types": "dist/index.d.ts", "bin": { "agentops-mcp": "dist/index.js" }, "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts", "prepublishOnly": "npm run build" }, "keywords": [ "mcp", "agentops", "observability", "ai", "monitoring" ], "author": "", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^0.4.0", "axios": "^1.6.0" }, "devDependencies": { "@types/node": "^20.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0" }, "engines": { "node": ">=18.0.0" }, "files": [ "dist/**/*", "README.md" ] } ``` -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- ```json { "compilerOptions": { "target": "ES2020", "module": "CommonJS", "lib": ["ES2020"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "declaration": true, "declarationMap": true, "sourceMap": true, "removeComments": false, "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, "exactOptionalPropertyTypes": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noUncheckedIndexedAccess": true, "moduleResolution": "node", "resolveJsonModule": true, "allowSyntheticDefaultImports": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] } ``` -------------------------------------------------------------------------------- /.github/workflows/issue-solver.yml: -------------------------------------------------------------------------------- ```yaml name: Entelligence-AI permissions: contents: read issues: write on: issues: types: [opened, edited] jobs: handle_issues: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js 20 uses: actions/setup-node@v4 with: node-version: '20' - name: Extract Repository and Username id: extract-repo-info run: | repo_name=$(basename $GITHUB_REPOSITORY) username=$(dirname $GITHUB_REPOSITORY | cut -d'/' -f1) echo "REPO_NAME=$repo_name" >> $GITHUB_ENV echo "USERNAME=$username" >> $GITHUB_ENV - name: Sanitize Issue Body id: sanitize-body run: | sanitized_body=$(echo "${{ github.event.issue.body }}" | tr -d '\r' | tr '\n' ' ') echo "SANITIZED_BODY=${sanitized_body}" >> $GITHUB_ENV - name: Debug Sanitized Body run: | echo "Sanitized Body: ${{ env.SANITIZED_BODY }}" - name: Call API id: call-api env: API_URL: ${{ secrets.ENTELLIGENCE_AI_ISSUE_API }} ISSUE_TITLE: ${{ github.event.issue.title }} ISSUE_BODY: ${{ env.SANITIZED_BODY }} REPO_NAME: ${{ env.REPO_NAME }} USERNAME: ${{ env.USERNAME }} run: | set +e response=$(curl -s -X POST ${{env.API_URL}} \ -H "Content-Type: application/json" \ -d "{\"vectorDBUrl\": \"${{env.USERNAME}}&${{env.REPO_NAME}}\", \"title\": \"${{env.ISSUE_TITLE}}\", \"summary\": \"${{env.ISSUE_BODY}}\", \"repoName\": \"${{env.USERNAME}}/${{env.REPO_NAME}}\"}") body=$(echo "$response" | sed '$d') echo "$response" echo "API_RESPONSE<<EOF" >> $GITHUB_ENV echo $(printf "%s" "$body" | base64) >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV set -e - name: Post Comment on Issue uses: actions/github-script@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const issueNumber = context.issue.number; const apiResponse = Buffer.from(process.env.API_RESPONSE, 'base64').toString('utf-8'); github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: issueNumber, body: apiResponse }); ``` -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- ```typescript #!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js"; import axios, { AxiosResponse } from "axios"; const HOST = "https://api.agentops.ai"; interface AuthHeaders { [key: string]: string; Authorization: string; } interface ErrorResponse { error: string; } // Server state to hold JWT token class ServerState { private jwtToken: string | null = null; setJwtToken(token: string): void { this.jwtToken = token; } getAuthHeaders(): AuthHeaders | null { if (!this.jwtToken) { return null; } return { Authorization: `Bearer ${this.jwtToken}`, "User-Agent": "agentops-mcp/0.3.5", }; } isAuthenticated(): boolean { return this.jwtToken !== null; } clearAuth(): void { this.jwtToken = null; } } const serverState = new ServerState(); /** * Initialize authentication on server startup if API key is available */ async function initializeAuth(): Promise<void> { const apiKey = process.env["AGENTOPS_API_KEY"]; if (apiKey) { try { const authResult = await authWithApiKey(apiKey); if (typeof authResult === "string") { serverState.setJwtToken(authResult); console.error( "Auto-authentication successful using environment variable", ); } else { console.error(`Auto-authentication failed: ${authResult.error}`); } } catch (error) { console.error( `Auto-authentication error: ${error instanceof Error ? error.message : String(error)}`, ); } } } /** * Authorize using an AgentOps project API key and return JWT token */ async function authWithApiKey(apiKey: string): Promise<string | ErrorResponse> { const data = { api_key: apiKey }; try { const response: AxiosResponse = await axios.post( `${HOST}/public/v1/auth/access_token`, data, ); const bearer = response.data?.bearer; if (!bearer) { throw new Error("No bearer token received from auth endpoint"); } return bearer; } catch (error) { return { error: error instanceof Error ? error.message : String(error) }; } } /** * Clean response by removing empty values */ function clean(response: any): any { if (typeof response === "object" && response !== null) { if (Array.isArray(response)) { return response .map((item) => clean(item)) .filter( (value) => value !== "" && value !== null && !(Array.isArray(value) && value.length === 0) && !(typeof value === "object" && Object.keys(value).length === 0), ); } else { const cleaned: any = {}; for (const [key, value] of Object.entries(response)) { const cleanedValue = clean(value); if ( cleanedValue !== "" && cleanedValue !== null && !(Array.isArray(cleanedValue) && cleanedValue.length === 0) && !( typeof cleanedValue === "object" && Object.keys(cleanedValue).length === 0 ) ) { cleaned[key] = cleanedValue; } } return cleaned; } } return response; } /** * Make authenticated request to AgentOps API using stored JWT token */ async function makeAuthenticatedRequest(endpoint: string): Promise<any> { const authHeaders = serverState.getAuthHeaders(); if (!authHeaders) { throw new Error( "Not authenticated. Please use the 'auth' tool first with your AgentOps API key.", ); } try { const response = await axios.get(`${HOST}${endpoint}`, { headers: authHeaders, }); return clean(response.data); } catch (error) { throw new Error(error instanceof Error ? error.message : String(error)); } } const server = new Server({ name: "agentops-mcp", version: "0.3.0", }); // List available tools server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "auth", description: "Authorize using the AGENTOPS_API_KEY. If the API key is not provided and cannot be found in the directory, ask the user for the API key.", inputSchema: { type: "object", properties: { api_key: { type: "string", description: "AgentOps project API key (optional if AGENTOPS_API_KEY environment variable is set)", }, }, required: [], }, }, { name: "get_trace", description: "Get trace information and metrics by trace_id.", inputSchema: { type: "object", properties: { trace_id: { type: "string", description: "Trace ID", }, }, required: ["trace_id"], }, }, { name: "get_span", description: "Get span information and metrics by span_id.", inputSchema: { type: "object", properties: { span_id: { type: "string", description: "Span ID", }, }, required: ["span_id"], }, }, { name: "get_complete_trace", description: "Reserved for explicit requests for COMPLETE or ALL data. Get complete trace information and metrics by trace_id.", inputSchema: { type: "object", properties: { trace_id: { type: "string", description: "Trace ID", }, }, required: ["trace_id"], }, }, ], }; }); // Handle tool calls server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; try { switch (name) { case "auth": { const { api_key } = args as { api_key?: string }; // Check if already authenticated if (serverState.isAuthenticated() && !api_key) { return { content: [ { type: "text", text: JSON.stringify( { success: true, message: "Already authenticated", source: "Previously authenticated (likely from environment variable on startup)", }, null, 2, ), }, ], }; } // Try to get API key from environment first, then from parameter const actualApiKey = api_key || process.env["AGENTOPS_API_KEY"]; if (!actualApiKey) { throw new Error( "No project API key available. Please provide a project API key.", ); } const authResult = await authWithApiKey(actualApiKey); if (typeof authResult === "object" && "error" in authResult) { throw new Error(`Authentication failed: ${authResult.error}`); } // Store the JWT token in server state serverState.setJwtToken(authResult); const result = await makeAuthenticatedRequest(`/public/v1/project`); const name = result.name; return { content: [ { type: "text", text: JSON.stringify( { success: true, message: "Authentication successful", project: name, }, null, 2, ), }, ], }; } case "get_trace": { const { trace_id } = args as { trace_id: string }; const [traceInfo, traceMetrics] = await Promise.all([ makeAuthenticatedRequest(`/public/v1/traces/${trace_id}`), makeAuthenticatedRequest(`/public/v1/traces/${trace_id}/metrics`), ]); const result = { ...traceInfo, metrics: traceMetrics }; return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } case "get_span": { const { span_id } = args as { span_id: string }; const [spanInfo, spanMetrics] = await Promise.all([ makeAuthenticatedRequest(`/public/v1/spans/${span_id}`), makeAuthenticatedRequest(`/public/v1/spans/${span_id}/metrics`), ]); const result = { ...spanInfo, metrics: spanMetrics }; return { content: [ { type: "text", text: JSON.stringify(result, null, 2), }, ], }; } case "get_complete_trace": { const { trace_id } = args as { trace_id: string }; const [traceInfo, traceMetrics] = await Promise.all([ makeAuthenticatedRequest(`/public/v1/traces/${trace_id}`), makeAuthenticatedRequest(`/public/v1/traces/${trace_id}/metrics`), ]); const parentTrace = { ...traceInfo, metrics: traceMetrics }; if (parentTrace.spans && Array.isArray(parentTrace.spans)) { for (let i = 0; i < parentTrace.spans.length; i++) { if (parentTrace.spans[i].span_id) { const span_id = parentTrace.spans[i].span_id; const [childSpanInfo, childSpanMetrics] = await Promise.all([ makeAuthenticatedRequest(`/public/v1/spans/${span_id}`), makeAuthenticatedRequest(`/public/v1/spans/${span_id}/metrics`), ]); parentTrace.spans[i] = { ...childSpanInfo, metrics: childSpanMetrics, }; } } } return { content: [ { type: "text", text: JSON.stringify(parentTrace, null, 2), }, ], }; } default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); return { content: [ { type: "text", text: JSON.stringify({ error: errorMessage }, null, 2), }, ], }; } }); async function main() { const transport = new StdioServerTransport(); // Initialize authentication before connecting await initializeAuth(); await server.connect(transport); // Debug: Log environment variable and authentication status const hasApiKey = !!process.env["AGENTOPS_API_KEY"]; const isAuthenticated = serverState.isAuthenticated(); console.error("AgentOps MCP server running on stdio"); console.error( `AGENTOPS_API_KEY environment variable: ${hasApiKey ? "SET" : "NOT SET"}`, ); console.error( `Authentication status: ${isAuthenticated ? "AUTHENTICATED" : "NOT AUTHENTICATED"}`, ); if (hasApiKey) { const keyPreview = process.env["AGENTOPS_API_KEY"]!.substring(0, 8) + "..."; console.error(`API Key preview: ${keyPreview}`); } } // Handle process signals gracefully process.on("SIGINT", () => { console.error("Received SIGINT, shutting down gracefully"); process.exit(0); }); process.on("SIGTERM", () => { console.error("Received SIGTERM, shutting down gracefully"); process.exit(0); }); // Handle uncaught exceptions process.on("uncaughtException", (error) => { console.error("Uncaught exception:", error); process.exit(1); }); process.on("unhandledRejection", (reason, promise) => { console.error("Unhandled rejection at:", promise, "reason:", reason); process.exit(1); }); if (require.main === module) { main().catch((error) => { console.error("Server error:", error); console.error("Stack trace:", error.stack); process.exit(1); }); } ```