19 |
20 | # metoro-mcp-server
21 | This repository contains th Metoro MCP (Model Context Protocol) Server. This MCP Server allows you to interact with your Kubernetes cluster via the Claude Desktop App!
22 |
23 | ## What is MCP (Model Context Protocol)?
24 | You can read more about the Model Context Protocol here: https://modelcontextprotocol.io
25 |
26 | But in a nutshell
27 | > The Model Context Protocol (MCP) is an open protocol that enables seamless integration between LLM applications and external data sources and tools. Whether you’re building an AI-powered IDE, enhancing a chat interface, or creating custom AI workflows, MCP provides a standardized way to connect LLMs with the context they need.
28 |
29 | ## What is Metoro?
30 | [Metoro](https://metoro.io/) is an observability platform designed for microservices running in Kubernetes and uses eBPF based instrumentation to generate deep telemetry without code changes.
31 | The data that is generated by the eBPF agents is sent to Metoro's backend to be stored and in the Metoro frontend using our apis.
32 |
33 | This MCP server exposes those APIs to an LLM so you can ask your AI questions about your Kubernetes cluster.
34 |
35 | ## Demo
36 |
37 | https://github.com/user-attachments/assets/b3f21e9a-45b8-4c17-8d8c-cff560d8694f
38 |
39 | ## How can I use Metoro MCP Server?
40 | 1. Install the [Claude Desktop App](https://claude.ai/download).
41 | 2. Make sure you have [Golang](https://golang.org/dl/) installed. `brew install go` for mac or `sudo apt-get install golang` for ubuntu.
42 | 3. Clone the repository: `git clone https://github.com/metoro-io/metoro-mcp-server.git`
43 | 4. Navigate to the repository directory: `cd metoro-mcp-server`
44 | 5. Build the server executable: `go build -o metoro-mcp-server`
45 |
46 | ### If you already have a Metoro Account:
47 | Copy your auth token from your Metoro account in [Settings](https://us-east.metoro.io/settings) -> Users Settings.
48 | Create a file in `~/Library/Application Support/Claude/claude_desktop_config.json` with the following contents:
49 | ```json
50 | {
51 | "mcpServers": {
52 | "metoro-mcp-server": {
53 | "command": "/metoro-mcp-server",
54 | "args": [],
55 | "env": {
56 | "METORO_AUTH_TOKEN" : "",
57 | "METORO_API_URL": "https://us-east.metoro.io"
58 | }
59 | }
60 | }
61 | }
62 | ```
63 |
64 | ### If you don't have a Metoro Account:
65 | No worries, you can still play around using the [Live Demo Cluster](https://demo.us-east.metoro.io/).
66 | The included token is a demo token, publicly available for anyone to use.
67 | Create a file in `~/Library/Application Support/Claude/claude_desktop_config.json` with the following contents:
68 | ```json
69 | {
70 | "mcpServers": {
71 | "metoro-mcp-server": {
72 | "command": "/metoro-mcp-server",
73 | "args": [],
74 | "env": {
75 | "METORO_AUTH_TOKEN" : "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjdXN0b21lcklkIjoiOThlZDU1M2QtYzY4ZC00MDRhLWFhZjItNDM2ODllNWJiMGUzIiwiZW1haWwiOiJ0ZXN0QGNocmlzYmF0dGFyYmVlLmNvbSIsImV4cCI6MTgyMTI0NzIzN30.7G6alDpcZh_OThYj293Jce5rjeOBqAhOlANR_Fl5auw",
76 | "METORO_API_URL": "https://demo.us-east.metoro.io"
77 | }
78 | }
79 | }
80 | }
81 | ```
82 |
83 | 4. Once you are done editing `claude_desktop_config.json` save the file and restart Claude Desktop app.
84 | 5. You should now see the Metoro MCP Server in the dropdown list of MCP Servers in the Claude Desktop App. You are ready to start using Metoro MCP Server with Claude Desktop App!
85 |
86 | ## Built with
87 |
88 | This server is built on top of our [Golang MCP SDK](https://github.com/metoro-io/mcp-golang).
89 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/metoro-io/metoro-mcp-server
2 |
3 | go 1.23
4 |
5 | require (
6 | github.com/google/uuid v1.6.0
7 | github.com/metoro-io/mcp-golang v0.7.0
8 | )
9 |
10 | require (
11 | github.com/bytedance/sonic v1.11.6 // indirect
12 | github.com/bytedance/sonic/loader v0.1.1 // indirect
13 | github.com/cloudwego/base64x v0.1.4 // indirect
14 | github.com/cloudwego/iasm v0.2.0 // indirect
15 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect
16 | github.com/gin-contrib/sse v0.1.0 // indirect
17 | github.com/go-playground/locales v0.14.1 // indirect
18 | github.com/go-playground/universal-translator v0.18.1 // indirect
19 | github.com/go-playground/validator/v10 v10.20.0 // indirect
20 | github.com/goccy/go-json v0.10.2 // indirect
21 | github.com/json-iterator/go v1.1.12 // indirect
22 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect
23 | github.com/leodido/go-urn v1.4.0 // indirect
24 | github.com/mattn/go-isatty v0.0.20 // indirect
25 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
26 | github.com/modern-go/reflect2 v1.0.2 // indirect
27 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect
28 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
29 | github.com/ugorji/go/codec v1.2.12 // indirect
30 | golang.org/x/arch v0.8.0 // indirect
31 | golang.org/x/crypto v0.23.0 // indirect
32 | golang.org/x/net v0.25.0 // indirect
33 | golang.org/x/sys v0.20.0 // indirect
34 | golang.org/x/text v0.15.0 // indirect
35 | google.golang.org/protobuf v1.34.1 // indirect
36 | )
37 |
38 | require (
39 | github.com/bahlo/generic-list-go v0.2.0 // indirect
40 | github.com/buger/jsonparser v1.1.1 // indirect
41 | github.com/gin-gonic/gin v1.10.0
42 | github.com/invopop/jsonschema v0.12.0 // indirect
43 | github.com/mailru/easyjson v0.7.7 // indirect
44 | github.com/pkg/errors v0.9.1 // indirect
45 | github.com/tidwall/gjson v1.18.0 // indirect
46 | github.com/tidwall/match v1.1.1 // indirect
47 | github.com/tidwall/pretty v1.2.1 // indirect
48 | github.com/tidwall/sjson v1.2.5 // indirect
49 | github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
50 | gopkg.in/yaml.v3 v3.0.1 // indirect
51 | )
52 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
2 | github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
3 | github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
4 | github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
5 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
6 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
7 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
8 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
9 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
10 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
11 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
12 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
13 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
15 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
16 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
17 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
18 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
19 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
20 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
21 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
22 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
23 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
24 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
25 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
26 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
27 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
28 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
29 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
30 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
31 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
32 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
33 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
34 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
35 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
36 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
37 | github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
38 | github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
39 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
40 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
41 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
42 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
43 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
44 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
45 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
46 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
47 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
48 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
49 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
50 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
51 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
52 | github.com/metoro-io/mcp-golang v0.7.0 h1:LM0AJylGqNg1+ZHWh9q/Jy8r7KGUwZkUYPmWYY7qNNw=
53 | github.com/metoro-io/mcp-golang v0.7.0/go.mod h1:ifLP9ZzKpN1UqFWNTpAHOqSvNkMK6b7d1FSZ5Lu0lN0=
54 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
55 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
56 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
57 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
58 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
59 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
60 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
61 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
62 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
63 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
65 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
66 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
67 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
68 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
69 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
70 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
71 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
72 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
73 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
74 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
75 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
76 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
77 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
78 | github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
79 | github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
80 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
81 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
82 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
83 | github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
84 | github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
85 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
86 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
87 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
88 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
89 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
90 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
91 | github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
92 | github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
93 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
94 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
95 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
96 | golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
97 | golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
98 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
99 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
100 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
101 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
102 | golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
103 | golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
104 | golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
105 | golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
106 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
107 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
108 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
109 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
110 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
111 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
112 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
113 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
114 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
115 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
116 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
117 |
--------------------------------------------------------------------------------
/images/Metoro_square.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/mcp-golang/transport/stdio"
9 | "github.com/metoro-io/metoro-mcp-server/resources"
10 | "github.com/metoro-io/metoro-mcp-server/tools"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | func main() {
15 | // Check if the appropriate environment variables are set
16 | if err := checkEnvVars(); err != nil {
17 | panic(err)
18 | }
19 |
20 | done := make(chan struct{})
21 |
22 | mcpServer := mcpgolang.NewServer(stdio.NewStdioServerTransport())
23 |
24 | // Add tools
25 | for _, tool := range tools.MetoroToolsList {
26 | err := mcpServer.RegisterTool(tool.Name, tool.Description, tool.Handler)
27 | if err != nil {
28 | panic(err)
29 | }
30 | }
31 |
32 | // Add resources
33 | for _, resource := range resources.MetoroResourcesList {
34 | err := mcpServer.RegisterResource(
35 | resource.Path,
36 | resource.Name,
37 | resource.Description,
38 | resource.ContentType,
39 | resource.Handler)
40 | if err != nil {
41 | panic(err)
42 | }
43 | }
44 |
45 | err := mcpServer.Serve()
46 | if err != nil {
47 | panic(err)
48 | }
49 |
50 | <-done
51 | }
52 |
53 | func checkEnvVars() error {
54 | if os.Getenv(utils.METORO_API_URL_ENV_VAR) == "" {
55 | return fmt.Errorf("%s environment variable not set", utils.METORO_API_URL_ENV_VAR)
56 | }
57 | if os.Getenv(utils.METORO_AUTH_TOKEN_ENV_VAR) == "" {
58 | return fmt.Errorf("%s environment variable not set", utils.METORO_AUTH_TOKEN_ENV_VAR)
59 | }
60 | return nil
61 | }
62 |
--------------------------------------------------------------------------------
/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // TODO: This file should be replaced if we can import the types from Metoro repo directly.
4 | // These are just duplicates at the moment. If updated in Metoro repository, it should also be updated here!
5 |
6 | type GetLogsRequest struct {
7 | // Required: Start time of when to get the logs in seconds since epoch
8 | StartTime int64 `json:"startTime"`
9 | // Required: End time of when to get the logs in seconds since epoch
10 | EndTime int64 `json:"endTime"`
11 | // The filters to apply to the logs, so for example, if you want to get logs for a specific service
12 | // you can pass in a filter like {"service_name": ["microservice_a"]}
13 | Filters map[string][]string `json:"filters"`
14 | // ExcludeFilters are filters that should be excluded from the logs
15 | // For example, if you want to get logs for all services except microservice_a you can pass in
16 | // {"service_name": ["microservice_a"]}
17 | ExcludeFilters map[string][]string `json:"excludeFilters"`
18 | // Previous page endTime in nanoseconds, used to get the next page of logs if there are more logs than the page size
19 | // If omitted, the first page of logs will be returned
20 | PrevEndTime *int64 `json:"prevEndTime"`
21 | // Regexes are used to filter logs based on a regex inclusively
22 | Regexes []string `json:"regexes"`
23 | // ExcludeRegexes are used to filter logs based on a regex exclusively
24 | ExcludeRegexes []string `json:"excludeRegexes"`
25 | Ascending bool `json:"ascending"`
26 | // The cluster/environments to get the logs for. If empty, all clusters will be included
27 | Environments []string `json:"environments"`
28 | }
29 |
30 | type GetTracesRequest struct {
31 | ServiceNames []string `json:"serviceNames"`
32 | StartTime int64 `json:"startTime"`
33 | EndTime int64 `json:"endTime"`
34 | Filters map[string][]string `json:"filters"`
35 | ExcludeFilters map[string][]string `json:"excludeFilters"`
36 | PrevEndTime *int64 `json:"prevEndTime"`
37 | Regexes []string `json:"regexes"`
38 | ExcludeRegexes []string `json:"excludeRegexes"`
39 | Ascending bool `json:"ascending"`
40 | Environments []string `json:"environments"`
41 | }
42 |
43 | type Aggregation string
44 |
45 | const (
46 | AggregationSum Aggregation = "sum"
47 | AggregationAvg Aggregation = "avg"
48 | AggregationMax Aggregation = "max"
49 | AggregationMin Aggregation = "min"
50 | AggregationCount Aggregation = "count"
51 | AggregationP50 Aggregation = "p50"
52 | AggregationP90 Aggregation = "p90"
53 | AggregationP95 Aggregation = "p95"
54 | AggregationP99 Aggregation = "p99"
55 |
56 | // Only for trace metrics
57 | AggregationRequestSize Aggregation = "requestSize"
58 | AggregationResponseSize Aggregation = "responseSize"
59 | AggregationTotalSize Aggregation = "totalSize"
60 | )
61 |
62 | type MetricFunction struct {
63 | // The type of the function
64 | FunctionType FunctionType `json:"functionType" jsonschema:"required,enum=monotonicDifference,enum=valueDifference,enum=perSecond,description=The type of the function to apply to the metric. Do not guess the function type. Use the available ones: perSecond or valueDifference or monotonicDifference."`
65 | //// The payload of the function
66 | //// TODO: If we have more payloads this can be an interface but for now its a math expression since its the only payload.
67 | //FunctionPayload MathExpression `json:"functionPayload" jsonschema:"description=The payload of the customMathExpression. this is only set for customMathExpression. "`
68 | }
69 |
70 | type MathExpression struct {
71 | Variables []string `json:"variables" jsonschema:"description=The variables to use in the math expression. For now this should always be ['a'] if set"`
72 | Expression string `json:"expression" jsonschema:"description=The math expression to apply to the metric. For example if you want to divide the metric by 60 you would set the expression as a / 60"`
73 | }
74 |
75 | type FunctionType string
76 |
77 | const (
78 | MonotonicDifference FunctionType = "monotonicDifference"
79 | ValueDifference FunctionType = "valueDifference"
80 | )
81 |
82 | type GetMetricRequest struct {
83 | // MetricName is the name of the metric to get
84 | MetricName string `json:"metricName" jsonschema:"required,description=Name of the metric to get the timeseries data for. Do not guess the metricName, get the possible values from get_metric_names tool"`
85 | // Required: Start time of when to get the logs in seconds since epoch
86 | StartTime int64 `json:"startTime" jsonschema:"required,description=Start time of when to get the metrics in seconds since epoch"`
87 | // Required: End time of when to get the logs in seconds since epoch
88 | EndTime int64 `json:"endTime" jsonschema:"required,description=Start time of when to get the metrics in seconds since epoch"`
89 | // The filters to apply to the logs, so for example, if you want to get logs for a specific service
90 | // you can pass in a filter like {"service_name": ["microservice_a"]}
91 | Filters map[string][]string `json:"filters"`
92 | // The filters to exclude from the logs, so for example, if you want to exclude logs for a specific service
93 | // you can pass in a filter like {"service_name": ["microservice_a"]}
94 | ExcludeFilters map[string][]string `json:"excludeFilters"`
95 | // Splits is a list of attributes to split the metrics by, for example, if you want to split the metrics by service
96 | // you can pass in a list like ["service_name"]
97 | Splits []string `json:"splits"`
98 | // Aggregation is the operation to apply to the metrics, for example, if you want to sum the metrics you can pass in "sum"
99 | Aggregation Aggregation `json:"aggregation"`
100 | // IsRate is a flag to indicate if the metric is a rate metric
101 | IsRate bool `json:"isRate"`
102 | // Functions is the list of functions to apply to the metric, in the same order that they appear in this array!!
103 | Functions []MetricFunction `json:"functions"`
104 | // LimitResults is a flag to indicate if the results should be limited.
105 | LimitResults bool `json:"limitResults"`
106 | // BucketSize is the size of each datapoint bucket in seconds
107 | BucketSize int64 `json:"bucketSize"`
108 | }
109 |
110 | type MetricAttributesRequest struct {
111 | StartTime int64 `json:"startTime"`
112 | EndTime int64 `json:"endTime"`
113 | MetricName string `json:"metricName"`
114 | FilterAttributes map[string][]string `json:"filterAttributes"`
115 | }
116 |
117 | type FuzzyMetricsRequest struct {
118 | MetricFuzzyMatch string `json:"metricFuzzyMatch"`
119 | Environments []string `json:"environments"`
120 | StartTime int64 `json:"startTime"`
121 | EndTime int64 `json:"endTime"`
122 | }
123 |
124 | type GetProfileRequest struct {
125 | // Required: ServiceName to get profiling for
126 | ServiceName string `json:"serviceName"`
127 |
128 | // Optional: ContainerNames to get profiling for
129 | ContainerNames []string `json:"containerNames"`
130 |
131 | // Required: Timestamp to get profiling after this time
132 | // Seconds since epoch
133 | StartTime int64 `json:"startTime"`
134 |
135 | // Required: Timestamp to get profiling this time
136 | // Seconds since epoch
137 | EndTime int64 `json:"endTime"`
138 | }
139 | type GetTraceMetricRequest struct {
140 | // Required: Start time of when to get the logs in seconds since epoch
141 | StartTime int64 `json:"startTime"`
142 | // Required: End time of when to get the logs in seconds since epoch
143 | EndTime int64 `json:"endTime"`
144 |
145 | // Optional: The name of the service to get the trace metrics for
146 | // Acts as an additional filter
147 | ServiceNames []string `json:"serviceNames"`
148 |
149 | // The filters to apply to the logs, so for example, if you want to get logs for a specific service
150 | //you can pass in a filter like {"service_name": ["microservice_a"]}
151 | Filters map[string][]string `json:"filters"`
152 |
153 | // The exclude filters to apply to the logs, so for example, if you want to exclude logs for a specific service
154 | //you can pass in a filter like {"service_name": ["microservice_a"]}
155 | ExcludeFilters map[string][]string `json:"excludeFilters"`
156 |
157 | // Regexes are used to filter traces based on a regex inclusively
158 | Regexes []string `json:"regexes"`
159 | // ExcludeRegexes are used to filter traces based on a regex exclusively
160 | ExcludeRegexes []string `json:"excludeRegexes"`
161 |
162 | // Splts is a list of attributes to split the metrics by, for example, if you want to split the metrics by service
163 | // you can pass in a list like ["service_name"]
164 | Splits []string `json:"splits"`
165 |
166 | // Functions is the array of function to apply to the trace metrics,
167 | //for example, if you want to get the monotonic difference between count of traces each minute.
168 | // Functions are applied in the same order that they appear in this array
169 | Functions []MetricFunction `json:"functions"`
170 |
171 | // Aggregate to apply to trace metrics, for example, if you want to sum the metrics you can pass in "sum"
172 | Aggregate Aggregation `json:"aggregate"`
173 |
174 | // Environments is a list of environments to filter the traces by. If empty, all environments will be included
175 | Environments []string `json:"environments"`
176 |
177 | // LimitResults is a flag to indicate if the results should be limited.
178 | LimitResults bool `json:"limitResults"`
179 |
180 | // BucketSize is the size of each datapoint bucket in seconds
181 | BucketSize int64 `json:"bucketSize"`
182 | }
183 |
184 | type GetSingleTraceSummaryRequest struct {
185 | TracesSummaryRequest
186 | // The attribute to get the summary for
187 | Attribute string `json:"attribute"`
188 | }
189 |
190 | type TracesSummaryRequest struct {
191 | // Required: Start time of when to get the service summaries in seconds since epoch
192 | StartTime int64 `json:"startTime"`
193 | // Required: End time of when to get the service summaries in seconds since epoch
194 | EndTime int64 `json:"endTime"`
195 |
196 | // The filters to apply to the trace summary, so for example, if you want to get traces for a specific service
197 | // you can pass in a filter like {"service_name": ["microservice_a"]}
198 | Filters map[string][]string `json:"filters"`
199 | // ExcludeFilters are used to exclude traces based on a filter
200 | ExcludeFilters map[string][]string `json:"excludeFilters"`
201 |
202 | // Regexes are used to filter traces based on a regex inclusively
203 | Regexes []string `json:"regexes"`
204 | // ExcludeRegexes are used to filter traces based on a regex exclusively
205 | ExcludeRegexes []string `json:"excludeRegexes"`
206 |
207 | // Optional: The name of the service to get the trace metrics for
208 | // Acts as an additional filter
209 | ServiceNames []string `json:"serviceNames"`
210 |
211 | // Environments is the environments to get the traces for. If empty, all environments will be included
212 | Environments []string `json:"environments"`
213 | }
214 |
215 | type GetK8sEventsRequest struct {
216 | // Required: Start time of when to get the k8s events in seconds since epoch
217 | StartTime int64 `json:"startTime"`
218 | // Required: End time of when to get the k8s events in seconds since epoch
219 | EndTime int64 `json:"endTime"`
220 | // The filters to apply to the k8s events, so for example, if you want to get k8s events for a specific service
221 | // you can pass in a filter like {"service_name": ["microservice_a"]}
222 | Filters map[string][]string `json:"filters"`
223 | // ExcludeFilters are filters that should be excluded from the k8s events
224 | // For example, if you want to get k8s events for all services except microservice_a you can pass in
225 | // {"service_name": ["microservice_a"]}
226 | ExcludeFilters map[string][]string `json:"excludeFilters"`
227 | // Previous page endTime in nanoseconds, used to get the next page of k8s events if there are more k8s events than the page size
228 | // If omitted, the first page of k8s events will be returned
229 | PrevEndTime *int64 `json:"prevEndTime"`
230 | // Regexes are used to filter k8s events based on a regex inclusively
231 | Regexes []string `json:"regexes"`
232 | // ExcludeRegexes are used to filter k8s events based on a regex exclusively
233 | ExcludeRegexes []string `json:"excludeRegexes"`
234 | // Ascending is a flag to determine if the k8s events should be returned in ascending order
235 | Ascending bool `json:"ascending"`
236 | // Environments is the environments to get the k8s events for
237 | Environments []string `json:"environments"`
238 | }
239 |
240 | type GetSingleK8sEventSummaryRequest struct {
241 | GetK8sEventsRequest
242 | // The attribute to get the summary for
243 | Attribute string `json:"attribute"`
244 | }
245 |
246 | type GetK8sEventMetricsRequest struct {
247 | // Required: Start time of when to get the logs in seconds since epoch
248 | StartTime int64 `json:"startTime"`
249 | // Required: End time of when to get the logs in seconds since epoch
250 | EndTime int64 `json:"endTime"`
251 |
252 | // The filters to apply to the logs, so for example, if you want to get logs for a specific service
253 | //you can pass in a filter like {"service_name": ["microservice_a"]}
254 | Filters map[string][]string `json:"filters"`
255 |
256 | // The exclude filters to apply to the logs, so for example, if you want to exclude logs for a specific service
257 | //you can pass in a filter like {"service_name": ["microservice_a"]}
258 | ExcludeFilters map[string][]string `json:"excludeFilters"`
259 |
260 | // Regexes are used to filter k8s events based on a regex inclusively
261 | Regexes []string `json:"regexes"`
262 | // ExcludeRegexes are used to filter k8s events based on a regex exclusively
263 | ExcludeRegexes []string `json:"excludeRegexes"`
264 |
265 | // Splts is a list of attributes to split the metrics by, for example, if you want to split the metrics by service
266 | // you can pass in a list like ["service_name"]
267 | Splits []string `json:"splits"`
268 |
269 | // OnlyNumRequests is a flag to only get the number of requests, this is a much faster query
270 | OnlyNumRequests bool `json:"onlyNumRequests"`
271 |
272 | // Environments is a list of environments to filter the k8s events by. If empty, all environments will be included
273 | Environments []string `json:"environments"`
274 | }
275 |
276 | type GetPodsRequest struct {
277 | // Required: Timestamp to get metadata updates after this time
278 | StartTime int64 `json:"startTime"`
279 |
280 | // Required: Timestamp to get metadata updates before this time
281 | EndTime int64 `json:"endTime"`
282 |
283 | // Optional: Environment to filter the pods by. If not provided, all environments are considered
284 | Environments []string `json:"environments"`
285 |
286 | // Optional: ServiceName to get metadata updates. One of ServiceName or NodeName is required
287 | ServiceName string `json:"serviceName"`
288 |
289 | // Optional: NodeName to get metadata updates. One of ServiceName or NodeName is required
290 | NodeName string `json:"nodeName"`
291 | }
292 |
293 | type LogSummaryRequest struct {
294 | // Required: Start time of when to get the service summaries in seconds since epoch
295 | StartTime int64 `json:"startTime"`
296 | // Required: End time of when to get the service summaries in seconds since epoch
297 | EndTime int64 `json:"endTime"`
298 | // The filters to apply to the log summary, so for example, if you want to get logs for a specific service
299 | // you can pass in a filter like {"service_name": ["microservice_a"]}
300 | Filters map[string][]string `json:"filters"`
301 |
302 | ExcludeFilters map[string][]string `json:"excludeFilters"`
303 | // RegexFilter is a regex to filter the logs
304 | Regexes []string `json:"regexes"`
305 | ExcludeRegexes []string `json:"excludeRegexes"`
306 | // The cluster/environments to get the logs for. If empty, all clusters will be included
307 | Environments []string `json:"environments"`
308 | }
309 |
310 | type GetSingleLogSummaryRequest struct {
311 | LogSummaryRequest
312 | // The attribute to get the summary for
313 | Attribute string `json:"attribute"`
314 | }
315 |
316 | type GetAllNodesRequest struct {
317 | // StartTime Required: Start time of when to get the nodes in seconds since epoch
318 | StartTime int64 `json:"startTime"`
319 | // EndTime Required: End time of when to get the nodes in seconds since epoch
320 | EndTime int64 `json:"endTime"`
321 | // Environments The cluster/environments to get the nodes for. If empty, all clusters will be included
322 | Environments []string `json:"environments"`
323 | // Filters The filters to apply to the nodes, so for example, if you want to get subset of nodes that have a specific label
324 | Filters map[string][]string `json:"filters"`
325 | // ExcludeFilters are filters that should be excluded from the nodes
326 | ExcludeFilters map[string][]string `json:"excludeFilters"`
327 | // Splits is a list of attributes to split the nodes by, for example, if you want to split the nodes a label
328 | Splits []string `json:"splits"`
329 | }
330 |
331 | type GetServiceSummariesRequest struct {
332 | // Required: Start time of when to get the service summaries in seconds
333 | StartTime int64 `json:"startTime"`
334 | // Required: End time of when to get the service summaries in seconds
335 | EndTime int64 `json:"endTime"`
336 | // If empty, all services across all environments will be returned
337 | Environments []string `json:"environments"`
338 | // Required: The namespace of the services to get summaries for. If empty, return services from all namespaces
339 | Namespace string `json:"namespace"`
340 | }
341 |
342 | // Dasboarding structs
343 | type SetDashboardRequest struct {
344 | Name string `json:"name"`
345 | Id string `json:"id"`
346 | DashboardJson string `json:"dashboardJson"`
347 | DefaultTimeRange string `json:"defaultTimeRange"`
348 | }
349 |
350 | // WidgetType is an enum representing different types of widgets
351 | type WidgetType string
352 |
353 | const (
354 | MetricChartWidgetType WidgetType = "MetricChart"
355 | GroupWidgetType WidgetType = "Group"
356 | MarkdownWidgetType WidgetType = "Markdown"
357 | )
358 |
359 | // WidgetPosition represents the position of a widget relative to its parent
360 | type WidgetPosition struct {
361 | X *int `json:"x,omitempty"`
362 | Y *int `json:"y,omitempty"`
363 | W *int `json:"w,omitempty" jsonschema:"required,description=The width of the widget. The dashboard is divided into 12 columns.For example a sensible value for a graph would be 6"`
364 | H *int `json:"h,omitempty" jsonschema:"required,description=The height of the widget. Each row is 128px. A sensible value for a graph would be 3."`
365 | }
366 |
367 | // Widget is the base interface for all widget types
368 | type Widget struct {
369 | WidgetType WidgetType `json:"widgetType" jsonschema:"required,description=The type of the widget. This can be MetricChart / Group / Markdown"`
370 | Position *WidgetPosition `json:"position,omitempty" jsonschema:"description=The position of the widget in the dashboard"`
371 | }
372 |
373 | //// Variable represents a configurable variable in a widget
374 | //type Variable struct {
375 | // Name string `json:"name"`
376 | // Key string `json:"key"`
377 | // DefaultValue string `json:"defaultValue"`
378 | // DefaultType MetricType `json:"defaultType"`
379 | // DefaultMetricName *string `json:"defaultMetricName,omitempty"`
380 | // IsOverrideable bool `json:"isOverrideable"`
381 | //}
382 |
383 | // GroupWidget represents a group of widgets
384 | type GroupWidget struct {
385 | Widget `json:",inline"`
386 | Title *string `json:"title,omitempty" jsonschema:"description=The title of the group widget if present"`
387 | Children []MetricChartWidget `json:"children" jsonschema:"description=The children widgets of the group widget. The children are MetricChartWidgets."`
388 | //Variables []Variable `json:"variables,omitempty"`
389 | }
390 |
391 | // MetricChartWidget represents a metric chart widget
392 | type MetricChartWidget struct {
393 | Widget `json:",inline"`
394 | MetricName string `json:"metricName" jsonschema:"description=The name of the metric to use in the chart if MetricType is metric. If MetricType is trace, this is not used and can be empty. This value is same as the metricName in the get_metric tool and the possible metricNames can be found in the get_metric_names tool"`
395 | Filters map[string][]string `json:"filters,omitempty" jsonschema:"description=The filters to apply to the metric. This is the same as the filters in the get_metric or get_trace_metric tool depending on the MetricType"`
396 | ExcludeFilters map[string][]string `json:"excludeFilters,omitempty" jsonschema:"description=The exclude filters to apply to the metric. This is the same as the exclude filters in the get_metric or get_trace_metric tool depending on the MetricType"`
397 | Splits []string `json:"splits,omitempty" jsonshcema:"description=Splits will allow you to group/split metrics by an attribute. This is useful if you would like to see the breakdown of a particular metric by an attribute. For example if you want to see the breakdown of the metric by service_name you would set the splits as ['service_name']"`
398 | Aggregation string `json:"aggregation" jsonschema:"description=The aggregation to apply to the metrics. This is the same as the aggregation in the get_metric or get_trace_metric tool depending on the MetricType"`
399 | Title *string `json:"title,omitempty" jsonschema:"description=The title of the metric chart widget if present"`
400 | Type ChartType `json:"type" jsonschema:"description=The type of the chart to display. Possible values are line / bar."`
401 | MetricType MetricType `json:"metricType" jsonschema:"description=The type of the metric to use in the chart. Possible values are metric / trace. If metric, the metricName should be used."`
402 | Functions []MetricFunction `json:"functions" jsonschema:"description=The functions to apply to the metric. This is the same as the functions in the get_metric or get_trace_metric tool depending on the MetricType"`
403 | }
404 |
405 | // MarkdownWidget represents a markdown content widget
406 | type MarkdownWidget struct {
407 | Widget `json:",inline"`
408 | Content string `json:"content"`
409 | }
410 | type ChartType string
411 |
412 | const (
413 | ChartTypeLine ChartType = "line"
414 | ChartTypeBar ChartType = "bar"
415 | )
416 |
417 | type MetricType string
418 |
419 | const (
420 | Metric MetricType = "metric" // please excuse the bad naming... this is a metric timeseries type.
421 | Trace MetricType = "trace" // trace timeseries type.
422 |
423 | Logs MetricType = "logs" // log timeseries type.
424 |
425 | KubernetesResource MetricType = "kubernetes_resource" // kubernetes resource timeseries type.
426 | )
427 |
428 | type GetLogMetricRequest struct {
429 | GetLogsRequest
430 | Splits []string `json:"splits" jsonschema:"description=Splits will allow you to group/split metrics by an attribute. This is useful if you would like to see the breakdown of a particular metric by an attribute. For example if you want to see the breakdown of the metric by service.name you would set the splits as ['service.name']"`
431 | Functions []MetricFunction `json:"functions" jsonschema:"description=The functions to apply to the log metric. Available functions are monotonicDifference which will calculate the difference between the current and previous value of the metric (negative values will be set to 0) and valueDifference which will calculate the difference between the current and previous value of the metric or MathExpression e.g. a / 60"`
432 | BucketSize int64 `json:"bucketSize" jsonschema:"description=The size of each datapoint bucket in seconds if not provided metoro will select the best bucket size for the given duration for performance and clarity"`
433 | }
434 |
435 | type GetKubernetesResourceRequest struct {
436 | // Required: Start time of when to get the service summaries in seconds since epoch
437 | StartTime int64 `json:"startTime"`
438 | // Required: End time of when to get the service summaries in seconds since epoch
439 | EndTime int64 `json:"endTime"`
440 | // The filters to apply to the kubernetes summary, so for example, if you want to get kubernetess for a specific service
441 | // you can pass in a filter like {"service.name": ["microservice_a"]}
442 | Filters map[string][]string `json:"filters"`
443 | // ExcludeFilters are filters that should be excluded from the kubernetes summary
444 | // For example, if you want to get kubernetess for all services except microservice_a you can pass in
445 | // {"service_name": ["microservice_a"]}
446 | ExcludeFilters map[string][]string `json:"excludeFilters"`
447 | // Splts is a list of attributes to split the metrics by, for example, if you want to split the metrics by service
448 | // you can pass in a list like ["service_name"]
449 | Splits []string `json:"splits"`
450 | // The cluster/environments to get the kubernetes metrics for. If empty, all clusters will be included
451 | Environments []string `json:"environments"`
452 | // Functions is the list of functions to apply to the metric, in the same order that they appear in this array!!
453 | Functions []MetricFunction `json:"functions"`
454 | // LimitResults is a flag to indicate if the results should be limited.
455 | LimitResults bool `json:"limitResults"`
456 | // BucketSize is the size of each datapoint bucket in seconds
457 | BucketSize int64 `json:"bucketSize"`
458 | // Aggregation is the operation to apply to the metrics, for example, if you want to sum the metrics you can pass in "sum"
459 | Aggregation Aggregation `json:"aggregation"`
460 | // JsonPath is a path to pull the json value from the metric
461 | JsonPath *string `json:"jsonPath"`
462 | }
463 |
464 | type GetMultiMetricRequest struct {
465 | // Required: Start time of when to get the service summaries in seconds
466 | StartTime int64 `json:"startTime"`
467 | // Required: End time of when to get the service summaries in seconds
468 | EndTime int64 `json:"endTime"`
469 | Metrics []SingleMetricRequest `json:"metrics" jsonschema:"required,description=Array of metrics to get the timeseries data for"`
470 | Formulas []Formula `json:"formulas" jsonschema:"description=Optional formulas to combine metrics/log metrics/trace metrics. Formula should only consist of formulaIdentifier of the metrics/logs/traces in the metrics array"`
471 | }
472 |
473 | type SingleMetricRequest struct {
474 | Type string `json:"type" jsonschema:"required,enum=metric,enum=trace,enum=logs,enum=kubernetes_resource,description=Type of metric to retrieve"`
475 | Metric *GetMetricRequest `json:"metric,omitempty" jsonschema:"description=Metric request details when type is 'metric'"`
476 | Trace *GetTraceMetricRequest `json:"trace,omitempty" jsonschema:"description=Trace metric request details when type is 'trace'"`
477 | Logs *GetLogMetricRequest `json:"logs,omitempty" jsonschema:"description=Log metric request details when type is 'logs'"`
478 | KubernetesResource *GetKubernetesResourceRequest `json:"kubernetes,omitempty" jsonschema:"description=Kubernetes resource request details when type is 'kubernetes_resource'"`
479 | ShouldNotReturn bool `json:"shouldNotReturn" jsonschema:"description=If true result won't be returned (useful for formulas)"`
480 | FormulaIdentifier string `json:"formulaIdentifier" jsonschema:"description=Identifier to reference this metric in formulas"`
481 | }
482 |
483 | type Formula struct {
484 | Formula string `json:"formula" jsonschema:"description=Math expression combining metric results using their formula identifiers"`
485 | }
486 |
487 | type GetMetricAttributesRequest struct {
488 | // Required: The metric name to get the summary for
489 | MetricName string `json:"metricName"`
490 | // Required: Start time of when to get the service summaries in seconds since epoch
491 | StartTime int64 `json:"startTime"`
492 | // Required: End time of when to get the service summaries in seconds since epoch
493 | EndTime int64 `json:"endTime"`
494 | // Environments is the environments to get the traces for. If empty, all environments will be included
495 | Environments []string `json:"environments"`
496 | }
497 |
498 | type MultiMetricAttributeKeysRequest struct {
499 | Type string `json:"type"`
500 | Metric *GetMetricAttributesRequest `json:"metric,omitempty"`
501 | // Currently trace and logs and kubernetes resource do not have any request parameters
502 | // Only metric has request parameters
503 | }
504 |
505 | type GetAttributeValuesRequest struct {
506 | Type MetricType `json:"type"`
507 | Attribute string `json:"attribute"`
508 | Limit *int `json:"limit"`
509 | Metric *GetMetricAttributesRequest `json:"metric,omitempty"`
510 | Trace *TracesSummaryRequest `json:"trace,omitempty"`
511 | Logs *LogSummaryRequest `json:"logs,omitempty"`
512 | Kubernetes *GetKubernetesResourceRequest `json:"kubernetes,omitempty"`
513 | }
514 |
515 | type GetAttributeKeysResponse struct {
516 | // The attribute values
517 | Attributes []string `json:"attributes"`
518 | }
519 |
520 | type GetMetricNamesResponse struct {
521 | MetricNames []string `json:"metrics"`
522 | }
523 |
--------------------------------------------------------------------------------
/resources/environments.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "context"
5 |
6 | mcpgolang "github.com/metoro-io/mcp-golang"
7 | "github.com/metoro-io/metoro-mcp-server/utils"
8 | )
9 |
10 | func EnvironmentResourceHandler() (*mcpgolang.ResourceResponse, error) {
11 | ctx := context.Background()
12 | response, err := utils.MakeMetoroAPIRequest("GET", "environments", nil, utils.GetAPIRequirementsFromRequest(ctx))
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | return mcpgolang.NewResourceResponse(
18 | mcpgolang.NewTextEmbeddedResource("api://environments", string(response), "text/plain")), nil
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/resources/k8s_events_attributes.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "context"
5 |
6 | mcpgolang "github.com/metoro-io/mcp-golang"
7 | "github.com/metoro-io/metoro-mcp-server/utils"
8 | )
9 |
10 | func K8sEventsAttributesResourceHandler() (*mcpgolang.ResourceResponse, error) {
11 | ctx := context.Background()
12 | resp, err := utils.MakeMetoroAPIRequest("GET", "k8s/events/summaryAttributes", nil, utils.GetAPIRequirementsFromRequest(ctx))
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | return mcpgolang.NewResourceResponse(
18 | mcpgolang.NewTextEmbeddedResource("api://k8sEventAttributes", string(resp), "text/plain")), nil
19 | }
20 |
--------------------------------------------------------------------------------
/resources/log_attributes.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "context"
5 |
6 | mcpgolang "github.com/metoro-io/mcp-golang"
7 | "github.com/metoro-io/metoro-mcp-server/utils"
8 | )
9 |
10 | func LogAttributesResourceHandler() (*mcpgolang.ResourceResponse, error) {
11 | ctx := context.Background()
12 | resp, err := utils.MakeMetoroAPIRequest("GET", "logsSummaryAttributes", nil, utils.GetAPIRequirementsFromRequest(ctx))
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | return mcpgolang.NewResourceResponse(
18 | mcpgolang.NewTextEmbeddedResource("api://logAttributes", string(resp), "text/plain")), nil
19 | }
20 |
--------------------------------------------------------------------------------
/resources/metrics.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "time"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | func MetricsResourceHandler() (*mcpgolang.ResourceResponse, error) {
15 | now := time.Now()
16 | twoHoursAgo := now.Add(-2 * time.Hour)
17 | request := model.FuzzyMetricsRequest{
18 | StartTime: twoHoursAgo.Unix(),
19 | EndTime: now.Unix(),
20 | MetricFuzzyMatch: "", // This will return all the metric names.
21 | Environments: []string{}, // All environments
22 | }
23 | jsonData, err := json.Marshal(request)
24 | if err != nil {
25 | return nil, err
26 | }
27 | ctx := context.Background()
28 | resp, err := utils.MakeMetoroAPIRequest("POST", "fuzzyMetricsNames", bytes.NewBuffer(jsonData), utils.GetAPIRequirementsFromRequest(ctx))
29 | if err != nil {
30 | return nil, err
31 | }
32 | return mcpgolang.NewResourceResponse(
33 | mcpgolang.NewTextEmbeddedResource("api://metrics", string(resp), "text/plain")), nil
34 | }
35 |
--------------------------------------------------------------------------------
/resources/namespaces.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "context"
5 |
6 | mcpgolang "github.com/metoro-io/mcp-golang"
7 | "github.com/metoro-io/metoro-mcp-server/utils"
8 | )
9 |
10 | func NamespacesResourceHandler() (*mcpgolang.ResourceResponse, error) {
11 | ctx := context.Background()
12 | response, err := utils.MakeMetoroAPIRequest("GET", "namespaces", nil, utils.GetAPIRequirementsFromRequest(ctx))
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | return mcpgolang.NewResourceResponse(
18 | mcpgolang.NewTextEmbeddedResource("api://namespaces", string(response), "text/plain")), nil
19 | }
20 |
--------------------------------------------------------------------------------
/resources/nodes.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "time"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | func NodesResourceHandler() (*mcpgolang.ResourceResponse, error) {
15 | now := time.Now()
16 | tenMinsAgo := now.Add(-10 * time.Minute)
17 | request := model.GetAllNodesRequest{
18 | StartTime: tenMinsAgo.Unix(),
19 | EndTime: now.Unix(),
20 | Filters: map[string][]string{},
21 | ExcludeFilters: map[string][]string{},
22 | Splits: []string{},
23 | Environments: []string{},
24 | }
25 | jsonRequest, err := json.Marshal(request)
26 | if err != nil {
27 | return nil, err
28 | }
29 | ctx := context.Background()
30 | response, err := utils.MakeMetoroAPIRequest("POST", "infrastructure/nodes", bytes.NewBuffer(jsonRequest), utils.GetAPIRequirementsFromRequest(ctx))
31 | if err != nil {
32 | return nil, err
33 | }
34 | return mcpgolang.NewResourceResponse(
35 | mcpgolang.NewTextEmbeddedResource("api://nodes", string(response), "text/plain")), nil
36 | }
37 |
--------------------------------------------------------------------------------
/resources/resources.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | type MetoroResource struct {
4 | Path string
5 | Name string
6 | Description string
7 | ContentType string
8 | Handler any
9 | }
10 |
11 | var MetoroResourcesList = []MetoroResource{
12 | {
13 | Path: "api://environments",
14 | Name: "environments",
15 | Description: "This resource provides a list of names of the kubernetes clusters/environments monitored by Metoro",
16 | ContentType: "text/plain",
17 | Handler: EnvironmentResourceHandler,
18 | },
19 | {
20 | Path: "api://namespaces",
21 | Name: "namespaces",
22 | Description: "This resource provides a list of namespaces in the kubernetes clusters/environments monitored by Metoro",
23 | ContentType: "text/plain",
24 | Handler: NamespacesResourceHandler,
25 | },
26 | {
27 | Path: "api://services",
28 | Name: "services",
29 | Description: "This resource provides a list of services running in the kubernetes clusters/environments monitored by Metoro",
30 | ContentType: "text/plain",
31 | Handler: ServicesResourceHandler,
32 | },
33 | {
34 | Path: "api://traceAttributes",
35 | Name: "traceAttributes",
36 | Description: "Provides a list of trace attribute keys that are available to be used for filtering or grouping traces. These trace attribute keys should be used as Filter/ExcludeFilter keys or Splits for get_traces, get_trace_metric and get_trace_attribute_values_for_individual_attribute tools arguments.",
37 | ContentType: "text/plain",
38 | Handler: TraceAttributesResourceHandler,
39 | },
40 | {
41 | Path: "api://k8sEventAttributes",
42 | Name: "k8sEventAttributes",
43 | Description: "Provides a list of Kubernetes Event's attribute keys that are available to be used for filtering or grouping K8s Events. These K8s Event attribute keys should be used as Filter/ExcludeFilter keys or Splits for get_k8s_events, get_k8s_events_volume and get_k8s_events_volume tools arguments.",
44 | ContentType: "text/plain",
45 | Handler: K8sEventsAttributesResourceHandler,
46 | },
47 | {
48 | Path: "api://metrics",
49 | Name: "metricNames",
50 | Description: "Provides a list of available metric names that can be used for as MetricName arguments to get_metric, get_metric_metadata and get_metric_attributes tools to get metrics data.",
51 | ContentType: "text/plain",
52 | Handler: MetricsResourceHandler,
53 | },
54 | {
55 | Path: "api://logAttributes",
56 | Name: "logAttributes",
57 | Description: "Provides a list of log attribute keys that are available to be used for filtering or grouping logs. These log attribute keys should be used as Filter/ExcludeFilter keys or Splits for get_logs, get_log_attribute_values_for_individual_attribute tools arguments.",
58 | ContentType: "text/plain",
59 | Handler: LogAttributesResourceHandler,
60 | },
61 | {
62 | Path: "api://nodes",
63 | Name: "nodes",
64 | Description: "Provides a list of nodes in the kubernetes clusters/environments monitored by Metoro. Any of these nodes/instances can be used as a filter/exclude for get_metric tool with the key 'kubernetes.io/hostname' and value as the node names in this resource.",
65 | ContentType: "text/plain",
66 | Handler: NodesResourceHandler,
67 | },
68 | }
69 |
--------------------------------------------------------------------------------
/resources/services.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "context"
5 |
6 | mcpgolang "github.com/metoro-io/mcp-golang"
7 | "github.com/metoro-io/metoro-mcp-server/utils"
8 | )
9 |
10 | func ServicesResourceHandler() (*mcpgolang.ResourceResponse, error) {
11 | ctx := context.Background()
12 | response, err := utils.MakeMetoroAPIRequest("GET", "services", nil, utils.GetAPIRequirementsFromRequest(ctx))
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | return mcpgolang.NewResourceResponse(
18 | mcpgolang.NewTextEmbeddedResource("api://services", string(response), "text/plain")), nil
19 | }
20 |
--------------------------------------------------------------------------------
/resources/trace_attributes.go:
--------------------------------------------------------------------------------
1 | package resources
2 |
3 | import (
4 | "context"
5 |
6 | mcpgolang "github.com/metoro-io/mcp-golang"
7 | "github.com/metoro-io/metoro-mcp-server/utils"
8 | )
9 |
10 | func TraceAttributesResourceHandler() (*mcpgolang.ResourceResponse, error) {
11 | ctx := context.Background()
12 | resp, err := utils.MakeMetoroAPIRequest("GET", "tracesSummaryAttributes", nil, utils.GetAPIRequirementsFromRequest(ctx))
13 | if err != nil {
14 | return nil, err
15 | }
16 |
17 | return mcpgolang.NewResourceResponse(
18 | mcpgolang.NewTextEmbeddedResource("api://traceAttributes", string(resp), "text/plain")), nil
19 | }
20 |
--------------------------------------------------------------------------------
/tools/create_dashboard.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | "github.com/google/uuid"
10 | mcpgolang "github.com/metoro-io/mcp-golang"
11 | "github.com/metoro-io/metoro-mcp-server/model"
12 | "github.com/metoro-io/metoro-mcp-server/utils"
13 | )
14 |
15 | type CreateDashboardHandlerArgs struct {
16 | DashboardName string `json:"dashboard_name" jsonschema:"required,description=The name of the dashboard to create"`
17 | GroupWidget model.GroupWidget `json:"group_widget" jsonschema:"required,description=The group widget this dashboard will have. This is the top level widget of the dashboard that will contain all other widgets. A widget can be either a group widget or a MetricChartWidget"`
18 | }
19 |
20 | func CreateDashboardHandler(ctx context.Context, arguments CreateDashboardHandlerArgs) (*mcpgolang.ToolResponse, error) {
21 | dashboardJson, err := json.Marshal(arguments.GroupWidget)
22 | if err != nil {
23 | return nil, fmt.Errorf("error marshaling dashboard properties: %v", err)
24 | }
25 |
26 | newDashboardRequest := model.SetDashboardRequest{
27 | Name: arguments.DashboardName,
28 | Id: uuid.NewString(),
29 | DashboardJson: string(dashboardJson),
30 | DefaultTimeRange: "1h",
31 | }
32 |
33 | resp, err := setDashboardMetoroCall(ctx, newDashboardRequest)
34 | if err != nil {
35 | return nil, fmt.Errorf("error setting dashboard: %v", err)
36 | }
37 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
38 | }
39 |
40 | func setDashboardMetoroCall(ctx context.Context, request model.SetDashboardRequest) ([]byte, error) {
41 | requestBody, err := json.Marshal(request)
42 | if err != nil {
43 | return nil, fmt.Errorf("error marshaling dashboard request: %v", err)
44 | }
45 | return utils.MakeMetoroAPIRequest("POST", "dashboard", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
46 | }
47 |
--------------------------------------------------------------------------------
/tools/get_alert_fires.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetAlertFiresHandlerArgs struct {
12 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get alert fires for. e.g. if you want to get alert fires for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
13 | AlertId string `json:"alert_id" jsonschema:"required,description=The ID of the alert to get the alert fires for"`
14 | }
15 |
16 | func GetAlertFiresHandler(ctx context.Context, arguments GetAlertFiresHandlerArgs) (*mcpgolang.ToolResponse, error) {
17 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
18 | if err != nil {
19 | return nil, fmt.Errorf("error calculating time range: %v", err)
20 | }
21 | body, err := getAlertFiresMetoroCall(ctx, arguments.AlertId, startTime, endTime)
22 | if err != nil {
23 | return nil, fmt.Errorf("error getting alert fires: %v", err)
24 | }
25 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
26 | }
27 |
28 | func getAlertFiresMetoroCall(ctx context.Context, alertId string, startTime, endTime int64) ([]byte, error) {
29 | return utils.MakeMetoroAPIRequest("GET", fmt.Sprintf("alertFires?alertId=%s&startTime=%d&endTime=%d", alertId, startTime, endTime), nil, utils.GetAPIRequirementsFromRequest(ctx))
30 | }
31 |
--------------------------------------------------------------------------------
/tools/get_alerts.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetAlertHandlerArgs struct{}
12 |
13 | func GetAlertsHandler(ctx context.Context, arguments GetAlertHandlerArgs) (*mcpgolang.ToolResponse, error) {
14 | body, err := getAlertsMetoroCall(ctx)
15 | if err != nil {
16 | return nil, fmt.Errorf("error getting alerts: %v", err)
17 | }
18 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
19 | }
20 |
21 | func getAlertsMetoroCall(ctx context.Context) ([]byte, error) {
22 | return utils.MakeMetoroAPIRequest("GET", "searchAlerts", nil, utils.GetAPIRequirementsFromRequest(ctx))
23 | }
24 |
--------------------------------------------------------------------------------
/tools/get_attribute_keys.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | mcpgolang "github.com/metoro-io/mcp-golang"
9 | "github.com/metoro-io/metoro-mcp-server/model"
10 | "github.com/metoro-io/metoro-mcp-server/utils"
11 | "slices"
12 | "strings"
13 | "time"
14 | )
15 |
16 | type GetAttributeKeysHandlerArgs struct {
17 | Type model.MetricType `json:"type" jsonschema:"required,description=The type of attribute keys to get. Either 'logs' or 'trace' or 'metric' or 'kubernetes_resource'"`
18 | TimeConfig utils.TimeConfig `json:"timeConfig" jsonschema:"required,description=The time period to get the possible attribute keys. e.g. if you want to get the possible values for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
19 | MetricName string `json:"metricName" jsonschema:"description=The name of the metric to get the possible attribute keys for. This is required if type is 'metric'"`
20 | }
21 |
22 | func GetAttributeKeysHandler(ctx context.Context, arguments GetAttributeKeysHandlerArgs) (*mcpgolang.ToolResponse, error) {
23 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
24 | if err != nil {
25 | return nil, fmt.Errorf("error calculating time range: %v", err)
26 | }
27 |
28 | if arguments.Type == model.Metric {
29 | if arguments.MetricName == "" {
30 | return nil, fmt.Errorf("metricName is required when type is 'metric'")
31 | }
32 |
33 | // Check whether the metric is valid. If not, return an error.
34 | err = CheckMetric(ctx, arguments.MetricName)
35 | if err != nil {
36 | return nil, err
37 | }
38 | }
39 |
40 | metricAttr := model.GetMetricAttributesRequest{
41 | StartTime: startTime,
42 | EndTime: endTime,
43 | MetricName: arguments.MetricName,
44 | Environments: []string{}, // TODO: Add environments to the request if needed. For now, we are not using it as I don't think its needed.
45 | }
46 |
47 | request := model.MultiMetricAttributeKeysRequest{
48 | Type: string(arguments.Type),
49 | Metric: &metricAttr,
50 | }
51 | jsonBody, err := json.Marshal(request)
52 | if err != nil {
53 | return nil, fmt.Errorf("error marshaling request: %v", err)
54 | }
55 |
56 | resp, err := utils.MakeMetoroAPIRequest("POST", "metrics/attributes", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
57 |
58 | if err != nil {
59 | return nil, fmt.Errorf("error making Metoro call: %v", err)
60 | }
61 |
62 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
63 | }
64 |
65 | func CheckMetric(ctx context.Context, metricName string) error {
66 | now := time.Now()
67 | hourAgo := now.Add(-30 * time.Minute)
68 | request := model.FuzzyMetricsRequest{
69 | StartTime: hourAgo.Unix(),
70 | EndTime: now.Unix(),
71 | MetricFuzzyMatch: "", // This will return all the metric names.
72 | }
73 | metricNamesResp, err := getMetricNamesMetoroCall(ctx, request)
74 |
75 | metricNames := model.GetMetricNamesResponse{}
76 | err = json.Unmarshal(metricNamesResp, &metricNames)
77 | if err != nil {
78 | return fmt.Errorf("error unmarshaling response: %v", err)
79 | }
80 |
81 | metricNamesStr := strings.Join(metricNames.MetricNames, ", ")
82 | if !slices.Contains(metricNames.MetricNames, metricName) {
83 | return fmt.Errorf("metricName '%s' is not valid. Valid metric names are: %s", metricName, metricNamesStr)
84 | }
85 |
86 | return nil
87 | }
88 |
--------------------------------------------------------------------------------
/tools/get_attribute_values.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | mcpgolang "github.com/metoro-io/mcp-golang"
9 | "github.com/metoro-io/metoro-mcp-server/model"
10 | "github.com/metoro-io/metoro-mcp-server/utils"
11 | )
12 |
13 | type GetAttributeValuesHandlerArgs struct {
14 | Type model.MetricType `json:"type" jsonschema:"required,description=The type of telemetry data to get the attribute keys and values for. Either 'logs' or 'trace' or 'metric' or 'kubernetes_resource'"`
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to use while getting the possible values of log attributes. e.g. if you want to get values for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | Attribute string `json:"attribute" jsonschema:"required,description=The attribute key to get the possible values for. Possible values for attribute should be obtained from get_attribute_keys tool call for the same type"`
17 | MetricName string `json:"metricName" jsonschema:"description=REQUIRED IF THE TYPE IS 'metric'. The name of the metric to get the possible attribute keys and values."`
18 | Filters map[string][]string `json:"filters" jsonschema:"description=The filters to apply before getting the possible values. For example if you want to get the possible values for an attribute key where the environment is X you would set the Filters as {environment: [X]}"`
19 | }
20 |
21 | func GetAttributeValuesHandler(ctx context.Context, arguments GetAttributeValuesHandlerArgs) (*mcpgolang.ToolResponse, error) {
22 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
23 | if err != nil {
24 | return nil, fmt.Errorf("error calculating time range: %v", err)
25 | }
26 |
27 | err = CheckAttributes(ctx, arguments.Type, arguments.Filters, map[string][]string{}, []string{}, nil)
28 | if err != nil {
29 | return nil, err
30 | }
31 |
32 | request := model.GetAttributeValuesRequest{
33 | Type: arguments.Type,
34 | Attribute: arguments.Attribute,
35 | }
36 |
37 | switch arguments.Type {
38 | case model.Logs:
39 | err = CheckAttributes(ctx, arguments.Type, arguments.Filters, map[string][]string{}, []string{}, nil)
40 | if err != nil {
41 | return nil, err
42 | }
43 | modelRequest := model.LogSummaryRequest{
44 | StartTime: startTime,
45 | EndTime: endTime,
46 | Filters: arguments.Filters,
47 | }
48 | request.Logs = &modelRequest
49 | break
50 | case model.Trace:
51 | err = CheckAttributes(ctx, arguments.Type, arguments.Filters, map[string][]string{}, []string{}, nil)
52 | if err != nil {
53 | return nil, err
54 | }
55 | modelRequest := model.TracesSummaryRequest{
56 | StartTime: startTime,
57 | EndTime: endTime,
58 | Filters: arguments.Filters,
59 | }
60 | request.Trace = &modelRequest
61 | break
62 | case model.Metric:
63 | err = CheckAttributes(ctx, arguments.Type, arguments.Filters, map[string][]string{}, []string{}, &model.GetMetricAttributesRequest{
64 | StartTime: startTime,
65 | EndTime: endTime,
66 | MetricName: arguments.MetricName,
67 | })
68 | if err != nil {
69 | return nil, err
70 | }
71 | modelRequest := model.GetMetricAttributesRequest{
72 | StartTime: startTime,
73 | EndTime: endTime,
74 | MetricName: arguments.MetricName,
75 | Environments: arguments.Filters["environment"],
76 | }
77 | request.Metric = &modelRequest
78 | break
79 | //case model.KubernetesResource:
80 | //
81 | // modelRequest := model.GetKubernetesResourceRequest{
82 | // StartTime: startTime,
83 | // EndTime: endTime,
84 | // Filters: arguments.Filters,
85 | // ExcludeFilters: arguments.Filters,
86 | // }
87 | // request.Kubernetes = &modelRequest
88 | // break
89 | default:
90 | return nil, fmt.Errorf("invalid type: %v", arguments.Type)
91 | }
92 | jsonBody, err := json.Marshal(request)
93 | if err != nil {
94 | return nil, fmt.Errorf("error marshaling request: %v", err)
95 | }
96 |
97 | resp, err := utils.MakeMetoroAPIRequest("POST", "metrics/attribute/values", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
98 |
99 | if err != nil {
100 | return nil, fmt.Errorf("error making Metoro call: %v", err)
101 | }
102 |
103 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
104 | }
105 |
--------------------------------------------------------------------------------
/tools/get_environments.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetEnvironmentHandlerArgs struct{}
12 |
13 | func GetEnvironmentsHandler(ctx context.Context, arguments GetEnvironmentHandlerArgs) (*mcpgolang.ToolResponse, error) {
14 | body, err := getEnvironmentsMetoroCall(ctx)
15 | if err != nil {
16 | return nil, fmt.Errorf("error getting environments: %v", err)
17 | }
18 |
19 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
20 | }
21 |
22 | func getEnvironmentsMetoroCall(ctx context.Context) ([]byte, error) {
23 | return utils.MakeMetoroAPIRequest("GET", "environments", nil, utils.GetAPIRequirementsFromRequest(ctx))
24 | }
25 |
--------------------------------------------------------------------------------
/tools/get_k8s_event_attribute_values.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetK8sEventAttributeValueHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get the possible values of K8 event attributes values. e.g. if you want to see the possible values for the attributes in the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | Attribute string `json:"attribute" jsonschema:"required,description=The attribute key to get the possible values for"`
17 | Filters map[string][]string `json:"filters" jsonschema:"description=The filters to apply before getting the possible values. For example if you want to get the possible values for attribute key service.name where the environment is X you would set the Filters as {environment: [X]}"`
18 | ExcludeFilters map[string][]string `json:"excludeFilters" jsonschema:"description=The exclude filters to exclude/eliminate possible values an attribute can take. Events matching the exclude filters will not be returned. For example if you want the possible values for attribute key service.name where the attribute environment is not X then you would set the ExcludeFilters as {environment: [X]}"`
19 | Regexes []string `json:"regexes" jsonschema:"description=The regexes to apply to the event messages. Only the attribute values (for a given attribute key) of events messages that match these regexes will be returned. For example if you want the possible values for attribute key service.name where the event message contains the word 'error' you would set the regexes as ['error']"`
20 | ExcludeRegexes []string `json:"excludeRegexes" jsonschema:"description=The exclude regexes to apply to the event messages. The attribute values (for a given attribute key) of events messages that match these regexes will not be returned. For example if you want the possible values for attribute key service.name where the event message does not contain the word 'error' you would set the exclude regexes as ['error']"`
21 | Environments []string `json:"environments" jsonschema:"description=The environments to get events from. If empty events from all environments will be returned"`
22 | Ascending bool `json:"ascending" jsonschema:"description=If true events will be returned in ascending order otherwise in descending order"`
23 | }
24 |
25 | func GetK8sEventAttributeValuesForIndividualAttributeHandler(ctx context.Context, arguments GetK8sEventAttributeValueHandlerArgs) (*mcpgolang.ToolResponse, error) {
26 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
27 | if err != nil {
28 | return nil, fmt.Errorf("error calculating time range: %v", err)
29 | }
30 | request := model.GetSingleK8sEventSummaryRequest{
31 | GetK8sEventsRequest: model.GetK8sEventsRequest{
32 | StartTime: startTime,
33 | EndTime: endTime,
34 | Filters: arguments.Filters,
35 | ExcludeFilters: arguments.ExcludeFilters,
36 | Regexes: arguments.Regexes,
37 | ExcludeRegexes: arguments.ExcludeRegexes,
38 | Environments: arguments.Environments,
39 | Ascending: arguments.Ascending,
40 | },
41 | Attribute: arguments.Attribute,
42 | }
43 |
44 | jsonBody, err := json.Marshal(request)
45 | if err != nil {
46 | return nil, fmt.Errorf("error marshaling request: %v", err)
47 | }
48 |
49 | resp, err := utils.MakeMetoroAPIRequest("POST", "k8s/events/summaryIndividualAttribute", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
50 | if err != nil {
51 | return nil, fmt.Errorf("error making Metoro call: %v", err)
52 | }
53 |
54 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
55 | }
56 |
--------------------------------------------------------------------------------
/tools/get_k8s_events.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetK8sEventsHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get k8s events for. e.g. if you want to get k8s events for the last 6 hours you would set time_period=6 and time_window=Hours. You can also set an absoulute time range by setting start_time and end_time"`
16 | Filters map[string][]string `json:"filters" jsonschema:"description=Filters to apply to the events. Only the events that match these filters will be returned. Get the possible filter keys from the get_k8s_events_attributes tool and possible filter values from the get_k8s_event_attribute_values tool (for a filter key)"`
17 | ExcludeFilters map[string][]string `json:"exclude_filters" jsonschema:"description=Filters to exclude the events. Events matching the exclude filters will not be returned. Get the possible exclude filter keys from the get_k8s_events_attributes tool and possible exclude filter values from the get_k8s_event_attribute_values tool (for a key)"`
18 | Regexes []string `json:"regexes" jsonschema:"description=Regexes to apply to the event messages. Only the events with messages that match these regexes will be returned. Regexes are ORed together. For example if you want to get events with messages that contain the word 'error' or 'warning' you would set the regexes as ['error' 'warning']"`
19 | ExcludeRegexes []string `json:"exclude_regexes" jsonschema:"description=Regexes to exclude the events. Events with messages that match these regexes will not be returned. Exclude regexes are AND together. For example if you want to get events with messages that do not contain the word 'error' or 'warning' you would set the exclude regexes as ['error' 'warning']"`
20 | Environments []string `json:"environments" jsonschema:"description=Environments/Clusters to get events for"`
21 | }
22 |
23 | func GetK8sEventsHandler(ctx context.Context, arguments GetK8sEventsHandlerArgs) (*mcpgolang.ToolResponse, error) {
24 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
25 | if err != nil {
26 | return nil, fmt.Errorf("error calculating time range: %v", err)
27 | }
28 | request := model.GetK8sEventsRequest{
29 | StartTime: startTime,
30 | EndTime: endTime,
31 | Filters: arguments.Filters,
32 | ExcludeFilters: arguments.ExcludeFilters,
33 | Regexes: arguments.Regexes,
34 | ExcludeRegexes: arguments.ExcludeRegexes,
35 | Environments: arguments.Environments,
36 | }
37 |
38 | jsonBody, err := json.Marshal(request)
39 | if err != nil {
40 | return nil, fmt.Errorf("error marshaling request: %v", err)
41 | }
42 |
43 | resp, err := utils.MakeMetoroAPIRequest("POST", "k8s/events", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
44 | if err != nil {
45 | return nil, fmt.Errorf("error making Metoro call: %v", err)
46 | }
47 |
48 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
49 | }
50 |
--------------------------------------------------------------------------------
/tools/get_k8s_events_attributes.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetK8sEventsAttributesHandlerArgs struct{}
12 |
13 | func GetK8sEventsAttributesHandler(ctx context.Context, arguments GetK8sEventsAttributesHandlerArgs) (*mcpgolang.ToolResponse, error) {
14 | resp, err := utils.MakeMetoroAPIRequest("GET", "k8s/events/summaryAttributes", nil, utils.GetAPIRequirementsFromRequest(ctx))
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
20 | }
21 |
--------------------------------------------------------------------------------
/tools/get_k8s_events_volume.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetK8sEventsVolumeHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get events volumes for. e.g. if you want to get events for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | Filters map[string][]string `json:"filters" jsonschema:"description=Filters to apply to the events. Only the event matching these filters will be counted. Get the possible filter keys from the get_k8s_events_attributes tool and possible filter values from the get_k8s_event_attribute_values tool (for a filter key)"`
17 | ExcludeFilters map[string][]string `json:"excludeFilters" jsonschema:"description=Filters to exclude the events. Events matching the exclude filters will not be counted. Get the possible exclude filter keys from the get_k8s_events_attributes tool and possible exclude filter values from the get_k8s_event_attribute_values tool (for a key)"`
18 | Regexes []string `json:"regexes" jsonschema:"description=Only the events with messages that match these regexes will be counted"`
19 | ExcludeRegexes []string `json:"excludeRegexes" jsonschema:"description=Events with messages that match these regexes will not be counted"`
20 | Environments []string `json:"environments" jsonschema:"description=Environments to get events from"`
21 | }
22 |
23 | func GetK8sEventsVolumeHandler(ctx context.Context, arguments GetK8sEventsVolumeHandlerArgs) (*mcpgolang.ToolResponse, error) {
24 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
25 | if err != nil {
26 | return nil, fmt.Errorf("error calculating time range: %v", err)
27 | }
28 | request := model.GetK8sEventMetricsRequest{
29 | StartTime: startTime,
30 | EndTime: endTime,
31 | Filters: arguments.Filters,
32 | ExcludeFilters: arguments.ExcludeFilters,
33 | Regexes: arguments.Regexes,
34 | ExcludeRegexes: arguments.ExcludeRegexes,
35 | Environments: arguments.Environments,
36 | Splits: []string{"EventType"}, // We want the volume to be split by EventType so we can see the breakdown of Warning/Normal events.
37 | }
38 |
39 | jsonBody, err := json.Marshal(request)
40 | if err != nil {
41 | return nil, fmt.Errorf("error marshaling request: %v", err)
42 | }
43 |
44 | resp, err := utils.MakeMetoroAPIRequest("POST", "k8s/events/metrics", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
45 | if err != nil {
46 | return nil, fmt.Errorf("error making Metoro call: %v", err)
47 | }
48 |
49 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
50 | }
51 |
--------------------------------------------------------------------------------
/tools/get_k8s_service_information.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetK8sServiceInformationHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time to get state of the YAML file. e.g. if you want to see the state of the service 5 minutes ago you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | ServiceName string `json:"serviceName" jsonschema:"required,description=The name of the service to get YAML file for."`
17 | Environments []string `json:"environments" jsonschema:"description=The environments to get service YAML for. If empty all environments will be used."`
18 | }
19 |
20 | func GetK8sServiceInformationHandler(ctx context.Context, arguments GetK8sServiceInformationHandlerArgs) (*mcpgolang.ToolResponse, error) {
21 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
22 | if err != nil {
23 | return nil, fmt.Errorf("error calculating time range: %v", err)
24 | }
25 | request := model.GetPodsRequest{
26 | StartTime: startTime,
27 | EndTime: endTime,
28 | ServiceName: arguments.ServiceName,
29 | Environments: arguments.Environments,
30 | }
31 | jsonBody, err := json.Marshal(request)
32 | if err != nil {
33 | return nil, fmt.Errorf("error marshaling request: %v", err)
34 | }
35 |
36 | resp, err := utils.MakeMetoroAPIRequest("POST", "k8s/summary", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
37 | if err != nil {
38 | return nil, fmt.Errorf("error making Metoro call: %v", err)
39 | }
40 |
41 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
42 | }
43 |
--------------------------------------------------------------------------------
/tools/get_log_attribute_values.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetLogAttributeValuesHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to use while getting the possible values of log attributes. e.g. if you want to get values for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | Attribute string `json:"attribute" jsonschema:"required,description=The attribute key to get the possible values for"`
17 | Filters map[string][]string `json:"filters" jsonschema:"description=The filters to apply before getting the possible values. For example if you want to get the possible values for attribute key service.name where the environment is X you would set the Filters as {environment: [X]}"`
18 | ExcludeFilters map[string][]string `json:"excludeFilters" jsonschema:"description=The exclude filters to exclude/eliminate possible values an attribute can take. Log attributes matching the exclude filters will not be returned. For example if you want the possible values for attribute key service.name where the attribute environment is not X then you would set the ExcludeFilters as {environment: [X]}"`
19 | Regexes []string `json:"regexes" jsonschema:"description=The regexes to apply to the log messages. Only the attribute values (for a given attribute key) of logs messages that match these regexes will be returned. For example if you want the possible values for attribute key service.name where the log message contains the word 'error' you would set the regexes as ['error']"`
20 | ExcludeRegexes []string `json:"excludeRegexes" jsonschema:"description=The exclude regexes to apply to the log messages. The attribute values (for a given attribute key) of log messages that match these regexes will not be returned. For example if you want the possible values for attribute key service.name where the log message does not contain the word 'error' you would set the exclude regexes as ['error']"`
21 | Environments []string `json:"environments" jsonschema:"description=The environments to get possible values of a log attributes for. If empty, possible values from all environments will be returned"`
22 | }
23 |
24 | func GetLogAttributeValuesForIndividualAttributeHandler(ctx context.Context, arguments GetLogAttributeValuesHandlerArgs) (*mcpgolang.ToolResponse, error) {
25 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
26 | if err != nil {
27 | return nil, fmt.Errorf("error calculating time range: %v", err)
28 | }
29 | request := model.GetSingleLogSummaryRequest{
30 | LogSummaryRequest: model.LogSummaryRequest{
31 | StartTime: startTime,
32 | EndTime: endTime,
33 | Filters: arguments.Filters,
34 | ExcludeFilters: arguments.ExcludeFilters,
35 | Regexes: arguments.Regexes,
36 | ExcludeRegexes: arguments.ExcludeRegexes,
37 | Environments: arguments.Environments,
38 | },
39 | Attribute: arguments.Attribute,
40 | }
41 |
42 | jsonBody, err := json.Marshal(request)
43 | if err != nil {
44 | return nil, fmt.Errorf("error marshaling request: %v", err)
45 | }
46 |
47 | resp, err := utils.MakeMetoroAPIRequest("POST", "logsSummaryIndividualAttribute", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
48 | if err != nil {
49 | return nil, fmt.Errorf("error making Metoro call: %v", err)
50 | }
51 |
52 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
53 | }
54 |
--------------------------------------------------------------------------------
/tools/get_log_attributes.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetLogAttributesHandlerArgs struct{}
12 |
13 | func GetLogAttributesHandler(ctx context.Context, arguments GetLogAttributesHandlerArgs) (*mcpgolang.ToolResponse, error) {
14 | resp, err := utils.MakeMetoroAPIRequest("GET", "logsSummaryAttributes", nil, utils.GetAPIRequirementsFromRequest(ctx))
15 | if err != nil {
16 | return nil, fmt.Errorf("error making Metoro call: %v", err)
17 | }
18 |
19 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
20 | }
21 |
--------------------------------------------------------------------------------
/tools/get_logs.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetLogsHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get the logs for. e.g. if you want the get the logs for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | Filters map[string][]string `json:"attributeFilters" jsonschema:"description=You must use get_attribute_keys and get_attribute_values before setting this. Log attributes to restrict the search to. Keys are anded together and values in the keys are ORed. e.g. {service.name: [/k8s/test/test /k8s/test/test2] namespace:[test]} will return all logs emited from (service.name = /k8s/test/test OR /k8s/test/test2) AND (namespace = test). Get the possible filter keys from the get_attribute_keys tool and possible values of a filter key from the get_attribute_values tool. If you are looking to get logs of a certain severity you should look up the log_level filter."`
17 | ExcludeFilters map[string][]string `json:"attributeExcludeFilters" jsonschema:"description=You must use get_attribute_keys and get_attribute_values before setting this.Log attributes to exclude from the search. Keys are anded together and values in the keys are ORed. e.g. {service.name: [/k8s/test/test /k8s/test/test2] namespace:[test]} will return all logs emited from NOT ((service.name = /k8s/test/test OR /k8s/test/test2) AND (namespace = test)). Get the possible filter keys from the get_attribute_keys tool and possible values of a filter key from the get_attribute_values tool. If you are looking to get logs of a certain severity you should look up the log_level filter."`
18 | Regex string `json:"regex" jsonschema:"description=Regex to apply to the log search re2 format. Any match in the log message will cause it to be returned. Use the filters parameter log_level if you want to look for logs of a certain severity"`
19 | Environments []string `json:"environments" jsonschema:"description=The environments to get logs from. If empty logs from all environments will be returned"`
20 | }
21 |
22 | func GetLogsHandler(ctx context.Context, arguments GetLogsHandlerArgs) (*mcpgolang.ToolResponse, error) {
23 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
24 | if err != nil {
25 | return nil, fmt.Errorf("error calculating time range: %v", err)
26 | }
27 |
28 | var regexes = []string{}
29 | if arguments.Regex != "" {
30 | regexes = append(regexes, arguments.Regex)
31 | }
32 |
33 | err = CheckAttributes(ctx, model.Logs, arguments.Filters, arguments.ExcludeFilters, []string{}, nil)
34 | if err != nil {
35 | return nil, err
36 | }
37 |
38 | request := model.GetLogsRequest{
39 | StartTime: startTime,
40 | EndTime: endTime,
41 | Filters: arguments.Filters,
42 | ExcludeFilters: arguments.ExcludeFilters,
43 | Regexes: regexes,
44 | Environments: arguments.Environments,
45 | }
46 |
47 | resp, err := getLogsMetoroCall(ctx, request)
48 | if err != nil {
49 | return nil, fmt.Errorf("error getting logs: %v", err)
50 | }
51 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
52 | }
53 |
54 | func getLogsMetoroCall(ctx context.Context, request model.GetLogsRequest) ([]byte, error) {
55 | requestBody, err := json.Marshal(request)
56 | if err != nil {
57 | return nil, fmt.Errorf("error marshaling logs request: %v", err)
58 | }
59 | return utils.MakeMetoroAPIRequest("POST", "logs", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
60 | }
61 |
--------------------------------------------------------------------------------
/tools/get_metric_attributes.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetMetricAttributesHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"timeConfig" jsonschema:"required,description=The time period to get the possible values of metric attributes for. e.g. if you want to get the possible values for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | MetricName string `json:"metricName" jsonschema:"required,description=The name of the metric to get the possible attribute keys and values."`
17 | FilterAttributes map[string][]string `json:"filterAttributes" jsonschema:"description=The attributes to filter the metric attributes by before getting the possible values. For example if you want to get the possible keys and values where the environment is X you would set the filterAttributes as {environment: [X]}"`
18 | }
19 |
20 | func GetMetricAttributesHandler(ctx context.Context, arguments GetMetricAttributesHandlerArgs) (*mcpgolang.ToolResponse, error) {
21 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
22 | if err != nil {
23 | return nil, fmt.Errorf("error calculating time range: %v", err)
24 | }
25 | request := model.MetricAttributesRequest{
26 | StartTime: startTime,
27 | EndTime: endTime,
28 | MetricName: arguments.MetricName,
29 | FilterAttributes: arguments.FilterAttributes,
30 | }
31 | response, err := getMetricAttributesMetoroCall(ctx, request)
32 | if err != nil {
33 | return nil, fmt.Errorf("error calling Metoro API: %v", err)
34 | }
35 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(response)))), nil
36 | }
37 |
38 | func getMetricAttributesMetoroCall(ctx context.Context, request model.MetricAttributesRequest) ([]byte, error) {
39 | jsonData, err := json.Marshal(request)
40 | if err != nil {
41 | return nil, err
42 | }
43 | return utils.MakeMetoroAPIRequest("POST", "metricAttributes", bytes.NewBuffer(jsonData), utils.GetAPIRequirementsFromRequest(ctx))
44 | }
45 |
--------------------------------------------------------------------------------
/tools/get_metric_metadata.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetMetricMetadataHandlerArgs struct {
12 | Name string `json:"name" jsonschema:"required,description=The name of the metric to get metadata for"`
13 | }
14 |
15 | func GetMetricMetadata(ctx context.Context, arguments GetMetricMetadataHandlerArgs) (*mcpgolang.ToolResponse, error) {
16 | response, err := getMetricMetadataMetoroCall(ctx, arguments.Name)
17 | if err != nil {
18 | return nil, fmt.Errorf("error calling Metoro get metric metadata api: %v", err)
19 | }
20 |
21 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(response)))), nil
22 | }
23 |
24 | func getMetricMetadataMetoroCall(ctx context.Context, metricName string) ([]byte, error) {
25 | return utils.MakeMetoroAPIRequest("GET", "metric/metadata?name="+metricName, nil, utils.GetAPIRequirementsFromRequest(ctx))
26 | }
27 |
--------------------------------------------------------------------------------
/tools/get_metric_names.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "time"
9 |
10 | mcpgolang "github.com/metoro-io/mcp-golang"
11 | "github.com/metoro-io/metoro-mcp-server/model"
12 | "github.com/metoro-io/metoro-mcp-server/utils"
13 | )
14 |
15 | type GetMetricNamesHandlerArgs struct {
16 | Environments []string `json:"environments" jsonschema:"description=Environments to get metrics names from. If empty all environments will be used."`
17 | }
18 |
19 | func GetMetricNamesHandler(ctx context.Context, arguments GetMetricNamesHandlerArgs) (*mcpgolang.ToolResponse, error) {
20 | now := time.Now()
21 | hourAgo := now.Add(-1 * time.Hour)
22 | request := model.FuzzyMetricsRequest{
23 | StartTime: hourAgo.Unix(),
24 | EndTime: now.Unix(),
25 | MetricFuzzyMatch: "", // This will return all the metric names.
26 | Environments: arguments.Environments,
27 | }
28 | response, err := getMetricNamesMetoroCall(ctx, request)
29 | if err != nil {
30 | return nil, fmt.Errorf("error calling Metoro API: %v", err)
31 | }
32 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(response)))), nil
33 | }
34 |
35 | func getMetricNamesMetoroCall(ctx context.Context, request model.FuzzyMetricsRequest) ([]byte, error) {
36 | jsonData, err := json.Marshal(request)
37 | if err != nil {
38 | return nil, err
39 | }
40 | return utils.MakeMetoroAPIRequest("POST", "fuzzyMetricsNames", bytes.NewBuffer(jsonData), utils.GetAPIRequirementsFromRequest(ctx))
41 | }
42 |
--------------------------------------------------------------------------------
/tools/get_multi_metric.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 | "slices"
9 | "strings"
10 |
11 | "github.com/metoro-io/metoro-mcp-server/model"
12 | "github.com/metoro-io/metoro-mcp-server/utils"
13 |
14 | mcpgolang "github.com/metoro-io/mcp-golang"
15 | )
16 |
17 | type GetMultiMetricHandlerArgs struct {
18 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get the timeseries data for. e.g. if you want to get the timeseries data for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
19 | Timeseries []SingleMetricRequest `json:"timeseries" jsonschema:"required,description=Array of timeseries data to get. Each item in this array corresponds to a single timeseries. You can then use the formulas to combine these timeseries. If you only want to see the combination of timeseries via defining formulas and if you dont want to see the individual timeseries data when setting formulas you can set shouldNotReturn to true"`
20 | Formulas []model.Formula `json:"formulas" jsonschema:"description=Optional formulas to combine timeseries. Formula should only consist of formulaIdentifier of the timeseries in the timeseries array. e.g. a + b + c if a b c appears in the formulaIdentifier of the timeseries array. You can ONLY do the following operations: Arithmetic operations:+ (for add) - (for substract) * (for multiply) / (for division) % (for modulus) ^ or ** (for exponent). Comparison: == != < > <= >= . Logical:! (for not) && (for AND) || (for OR). Conditional operations: ?: (ternary) e.g. (a || b) ? 1 : 0. Do not guess the operations. Just use these available ones!"`
21 | }
22 |
23 | // TODO: Add kubernetes resource request type attributes.
24 | type SingleMetricRequest struct {
25 | Type model.MetricType `json:"type" jsonschema:"required,enum=metric,enum=trace,enum=logs,enum=kubernetes_resource,description=Type of timeseries data to retrieve"`
26 | MetricName string `json:"metricName" jsonschema:"description=THIS IS ONLY REQUIRED IF THE type is 'metric'.The name of the metric to use for getting the timeseries data for type 'metric'. If metric name ends with _total metoro already accounts for rate differences when returning the value so you don't need to calculate the rate yourself."`
27 | Aggregation model.Aggregation `json:"aggregation" jsonschema:"required,enum=sum,enum=count,enum=min,enum=max,enum=avg,enum=p50,enum=p90,enum=p95,enum=p99,description=The aggregation to apply to the timeseries at the datapoint bucket size level. The aggregation will be applied to every datapoint bucket. For example if the bucket size is 1 minute and the aggregation is sum then the sum of all datapoints in a minute will be returned. Do not guess the aggregations. Use the available ones. For traces you can use count p50 p90 p95 p99. for logs its always count. For metrics you can use sum min max avg"`
28 | JsonPath *string `json:"jsonPath" jsonschema:"description=THIS IS ONLY BE SET IF THE type is 'kubernetes_resource' and the aggregate is not count. The json path to use to get the value from the kubernetes resource to plot. for example if this was spec.replicas then the value we return would be aggregate(spec.replicas)"`
29 | Filters map[string][]string `json:"filters" jsonschema:"description=Filters to apply to the timeseries. Only the timeseries that match these filters will be returned. You MUST call get_attribute_keys and get_attribute_values tools to get the valid filter keys and values. e.g. {service_name: [/k8s/namespaceX/serviceX]} should return timeseries for serviceX in namespaceX. This is just and example. Do not guess the attribute keys and values."`
30 | ExcludeFilters map[string][]string `json:"excludeFilters" jsonschema:"description=Filters to exclude the timeseries data. Timeseries matching the exclude filters will not be returned. You MUST call get_attribute_keys and get_attribute_values tools to get the valid filter keys and values. e.g. {service_name: [/k8s/namespaceX/serviceX]} should exclude timeseries from serviceX in namespaceX. This is just and example. Do not guess the attribute keys and values"`
31 | Splits []string `json:"splits" jsonschema:"description=Array of attribute keys to split/group by the timeseries data by. Splits will allow you to group timeseries data by an attribute. This is useful if you would like to see the breakdown of a particular timeseries by an attribute. Get the attributes that you can pass into as Splits from the get_attribute_keys tool. DO NOT GUESS THE ATTRIBUTES."`
32 | Regexes []string `json:"regexes" jsonschema:"description=This should only be set if the type is 'logs'. Regexes are evaluated against the log message/body. Only the timeseries (logs) data that match these regexes will be returned. Regexes are ANDed together. For example if you want to get log count with message that contains the words 'fish' and 'chips' you would set the regexes as ['fish' 'chips']"`
33 | ExcludeRegexes []string `json:"excludeRegexes" jsonschema:"description=This should only be set if the type is 'logs'. Exclude regexes are evaluated against the log message/body. Log timeseries data that match these regexes will not be returned. Exclude regexes are ORed together. For example if you want to get timeseries data with messages that do not contain the word 'fish' or 'chips' you would set the exclude regexes as ['fish' 'chips']"`
34 | BucketSize int64 `json:"bucketSize" jsonschema:"description=The size of each datapoint bucket in seconds if not provided metoro will select the best bucket size for the given duration for performance and clarity"`
35 | Functions []model.MetricFunction `json:"functions" jsonschema:"description=Array of functions to apply to the timeseries data in the order as it appears in the array. Functions will be applied to the timeseries data after the aggregation. For example if the aggregation is sum and the function is perSecond then the perSecond of the sum will be returned. Do not guess the functions. Use the available ones. For traces you can use rate. For logs you can use count. For metrics you can use rate sum min max avg. For kubernetes resources you can use rate sum min max avg"`
36 | ShouldNotReturn bool `json:"shouldNotReturn" jsonschema:"description=If true result won't be returned (useful for formulas). Only set this to true if you only want to see the combination of timeseries via defining formulas and if you dont want to see the individual timeseries data.'"`
37 | FormulaIdentifier string `json:"formulaIdentifier" jsonschema:"description=Identifier to reference this metric in formulas. These should be unique for timeseries that you are requesting. For ease of use you can just use the alpahabet letters. e.g. a b c d e f g h i j k l m n o p q r s t u v w x y z"`
38 | }
39 |
40 | func GetMultiMetricHandler(ctx context.Context, arguments GetMultiMetricHandlerArgs) (*mcpgolang.ToolResponse, error) {
41 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
42 | if err != nil {
43 | return nil, fmt.Errorf("error calculating time range: %v", err)
44 | }
45 |
46 | err = checkTimeseries(ctx, arguments.Timeseries, startTime, endTime)
47 | if err != nil {
48 | return nil, err
49 | }
50 |
51 | request := model.GetMultiMetricRequest{
52 | StartTime: startTime,
53 | EndTime: endTime,
54 | Metrics: convertTimeseriesToAPITimeseries(arguments.Timeseries, startTime, endTime),
55 | Formulas: arguments.Formulas,
56 | }
57 |
58 | if len(arguments.Timeseries) == 0 {
59 | return nil, fmt.Errorf("no timeseries data provided")
60 | }
61 |
62 | body, err := getMultiMetricMetoroCall(ctx, request)
63 | if err != nil {
64 | return nil, fmt.Errorf("error getting metric: %v", err)
65 | }
66 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
67 | }
68 |
69 | func getMultiMetricMetoroCall(ctx context.Context, request model.GetMultiMetricRequest) ([]byte, error) {
70 | requestBody, err := json.Marshal(request)
71 | if err != nil {
72 | return nil, fmt.Errorf("error marshaling metric request: %v", err)
73 | }
74 | return utils.MakeMetoroAPIRequest("POST", "metrics", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
75 | }
76 |
77 | func convertTimeseriesToAPITimeseries(timeseries []SingleMetricRequest, startTime int64, endTime int64) []model.SingleMetricRequest {
78 | result := make([]model.SingleMetricRequest, len(timeseries))
79 |
80 | for i, ts := range timeseries {
81 | apiRequest := model.SingleMetricRequest{
82 | Type: string(ts.Type),
83 | ShouldNotReturn: ts.ShouldNotReturn,
84 | FormulaIdentifier: ts.FormulaIdentifier,
85 | }
86 |
87 | switch ts.Type {
88 | case model.Metric:
89 | apiRequest.Metric = &model.GetMetricRequest{
90 | StartTime: startTime,
91 | EndTime: endTime,
92 | MetricName: ts.MetricName,
93 | Filters: ts.Filters,
94 | ExcludeFilters: ts.ExcludeFilters,
95 | Splits: ts.Splits,
96 | Aggregation: ts.Aggregation,
97 | Functions: ts.Functions,
98 | //Functions: ts.Metric.Functions,
99 | //LimitResults: ts.Metric.LimitResults,
100 | BucketSize: ts.BucketSize,
101 | }
102 |
103 | case model.Trace:
104 | apiRequest.Trace = &model.GetTraceMetricRequest{
105 | StartTime: startTime,
106 | EndTime: endTime,
107 | Filters: ts.Filters,
108 | ExcludeFilters: ts.ExcludeFilters,
109 | Splits: ts.Splits,
110 | Aggregate: ts.Aggregation,
111 | BucketSize: ts.BucketSize,
112 | Functions: ts.Functions,
113 | //ServiceNames: ts.ServiceNames,
114 | //Regexes: ts.Regexes,
115 | //ExcludeRegexes: ts.ExcludeRegexes,
116 | //Environments: ts.Environments,
117 | //LimitResults: ts.LimitResults,
118 | //
119 | }
120 |
121 | case model.Logs:
122 | apiRequest.Logs = &model.GetLogMetricRequest{
123 | GetLogsRequest: model.GetLogsRequest{
124 | StartTime: startTime,
125 | EndTime: endTime,
126 | Filters: ts.Filters,
127 | ExcludeFilters: ts.ExcludeFilters,
128 | Regexes: ts.Regexes,
129 | ExcludeRegexes: ts.ExcludeRegexes,
130 | //Environments: ts.Environments,
131 | },
132 | Functions: ts.Functions,
133 | Splits: ts.Splits,
134 | BucketSize: ts.BucketSize,
135 | //Functions: ts.Functions,
136 | }
137 | case model.KubernetesResource:
138 | apiRequest.KubernetesResource = &model.GetKubernetesResourceRequest{
139 | StartTime: startTime,
140 | EndTime: endTime,
141 | Filters: ts.Filters,
142 | ExcludeFilters: ts.ExcludeFilters,
143 | Splits: ts.Splits,
144 | BucketSize: ts.BucketSize,
145 | Functions: ts.Functions,
146 | JsonPath: ts.JsonPath,
147 | Aggregation: ts.Aggregation,
148 | }
149 | }
150 | result[i] = apiRequest
151 | }
152 |
153 | return result
154 | }
155 |
156 | func CheckAttributes(ctx context.Context, requestType model.MetricType, filters map[string][]string, excludeFilters map[string][]string, splits []string, metricRequest *model.GetMetricAttributesRequest) error {
157 | // Check whether the attributes given are valid.
158 | request := model.MultiMetricAttributeKeysRequest{
159 | Type: string(requestType),
160 | Metric: metricRequest,
161 | }
162 | jsonBody, err := json.Marshal(request)
163 | if err != nil {
164 | return fmt.Errorf("error marshaling request: %v", err)
165 | }
166 |
167 | attributeResp, err := utils.MakeMetoroAPIRequest("POST", "metrics/attributes", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
168 | if err != nil {
169 | return fmt.Errorf("error making Metoro call: %v", err)
170 | }
171 |
172 | attributeKeys := model.GetAttributeKeysResponse{}
173 | err = json.Unmarshal(attributeResp, &attributeKeys)
174 | if err != nil {
175 | return fmt.Errorf("error unmarshaling response: %v", err)
176 | }
177 |
178 | attributesAsString := strings.Join(attributeKeys.Attributes, ", ")
179 |
180 | // Check whether the filters given are valid.
181 | for key, _ := range filters {
182 | if !slices.Contains(attributeKeys.Attributes, key) {
183 | return fmt.Errorf("invalid filter key: %s. Valid filter keys are: %s. Please try again with a valid key", key, attributesAsString)
184 | }
185 | }
186 |
187 | for key, _ := range excludeFilters {
188 | if !slices.Contains(attributeKeys.Attributes, key) {
189 | return fmt.Errorf("invalid exclude filter key: %s. Valid keys are: %s. Please try again with a valid key", key, attributesAsString)
190 | }
191 | }
192 |
193 | for _, split := range splits {
194 | if !slices.Contains(attributeKeys.Attributes, split) {
195 | return fmt.Errorf("invalid split key: %s. Valid keys are: %s. Please try again with a valid key", split, attributesAsString)
196 | }
197 | }
198 | return nil
199 | }
200 |
201 | func checkTimeseries(ctx context.Context, timeseries []SingleMetricRequest, startTime, endTime int64) error {
202 | for _, ts := range timeseries {
203 | if ts.Type != model.Metric {
204 | continue
205 | }
206 | err := CheckMetric(ctx, ts.MetricName)
207 | if err != nil {
208 | return err
209 | }
210 | err = CheckAttributes(ctx, ts.Type, ts.Filters, ts.ExcludeFilters, ts.Splits, &model.GetMetricAttributesRequest{
211 | StartTime: startTime,
212 | EndTime: endTime,
213 | MetricName: ts.MetricName,
214 | })
215 | if err != nil {
216 | return err
217 | }
218 | }
219 | return nil
220 | }
221 |
--------------------------------------------------------------------------------
/tools/get_namespaces.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetNamespacesHandlerArgs struct{}
12 |
13 | func GetNamespacesHandler(ctx context.Context, arguments GetNamespacesHandlerArgs) (*mcpgolang.ToolResponse, error) {
14 | body, err := getNamespacesMetoroCall(ctx)
15 | if err != nil {
16 | return nil, fmt.Errorf("error getting namespaces: %v", err)
17 | }
18 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
19 | }
20 |
21 | func getNamespacesMetoroCall(ctx context.Context) ([]byte, error) {
22 | return utils.MakeMetoroAPIRequest("GET", "namespaces", nil, utils.GetAPIRequirementsFromRequest(ctx))
23 | }
24 |
--------------------------------------------------------------------------------
/tools/get_node_attributes.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/model"
9 | "github.com/metoro-io/metoro-mcp-server/utils"
10 | )
11 |
12 | type GetNodeAttributesHandlerArgs struct {
13 | TimeConfig utils.TimeConfig `json:"timeConfig" jsonschema:"required,description=The time range to get the node attributes for"`
14 | }
15 |
16 | func GetNodeAttributesHandler(ctx context.Context, arguments GetNodeAttributesHandlerArgs) (*mcpgolang.ToolResponse, error) {
17 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
18 | if err != nil {
19 | return nil, fmt.Errorf("error calculating time range: %v", err)
20 | }
21 | request := model.MetricAttributesRequest{
22 | StartTime: startTime,
23 | EndTime: endTime,
24 | MetricName: "node_info",
25 | FilterAttributes: map[string][]string{},
26 | }
27 | response, err := getMetricAttributesMetoroCall(ctx, request)
28 | if err != nil {
29 | return nil, fmt.Errorf("error calling Metoro API: %v", err)
30 | }
31 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(response)))), nil
32 | }
33 |
--------------------------------------------------------------------------------
/tools/get_node_info.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "time"
7 |
8 | mcpgolang "github.com/metoro-io/mcp-golang"
9 | "github.com/metoro-io/metoro-mcp-server/utils"
10 | )
11 |
12 | type GetNodeInfoHandlerArgs struct {
13 | NodeName string `json:"nodeName" jsonschema:"required,description=The name of the node to get the YAML/information for"`
14 | }
15 |
16 | func GetNodeInfoHandler(ctx context.Context, arguments GetNodeInfoHandlerArgs) (*mcpgolang.ToolResponse, error) {
17 | now := time.Now()
18 | fiveMinsAgo := now.Add(-10 * time.Minute)
19 |
20 | body, err := getNodeInfoMetoroCall(ctx, arguments.NodeName, fiveMinsAgo.Unix())
21 | if err != nil {
22 | return nil, fmt.Errorf("error getting node info: %v", err)
23 | }
24 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
25 | }
26 |
27 | func getNodeInfoMetoroCall(ctx context.Context, nodeName string, startTime int64) ([]byte, error) {
28 | return utils.MakeMetoroAPIRequest("GET", fmt.Sprintf("infrastructure/node?nodeName=%s&startTime=%d", nodeName, startTime), nil, utils.GetAPIRequirementsFromRequest(ctx))
29 | }
30 |
--------------------------------------------------------------------------------
/tools/get_nodes.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetNodesHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get nodes for. e.g. if you want to get nodes for the last 5 minutes you would set time_period=5 and time_window=Minutes or if you want to get nodes for the last 2 hours you would set time_period=2 and time_window=Hours. You can also set an absoulute time range by setting start_time and end_time"`
16 | Filters map[string][]string `json:"filters" jsonschema:"description=The filters to apply to the nodes. Only the nodes that match these filters will be returned. To get possible filter keys and values use the get_node_attributes tool."`
17 | ExcludeFilters map[string][]string `json:"excludeFilters" jsonschema:"description=The filters to exclude the nodes. Nodes matching the exclude filters will not be returned. To get possible exclude filter keys and values use the get_node_attributes tool."`
18 | Environments []string `json:"environments" jsonschema:"description=The environments to get nodes that belong to. If empty all environments will be used."`
19 | }
20 |
21 | func GetNodesHandler(ctx context.Context, arguments GetNodesHandlerArgs) (*mcpgolang.ToolResponse, error) {
22 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
23 | if err != nil {
24 | return nil, fmt.Errorf("error calculating time range: %v", err)
25 | }
26 | request := model.GetAllNodesRequest{
27 | StartTime: startTime,
28 | EndTime: endTime,
29 | Filters: arguments.Filters,
30 | ExcludeFilters: arguments.ExcludeFilters,
31 | Splits: []string{},
32 | Environments: arguments.Environments,
33 | }
34 | body, err := getNodesMetoroCall(ctx, request)
35 | if err != nil {
36 | return nil, fmt.Errorf("error getting nodes: %v", err)
37 | }
38 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
39 | }
40 |
41 | func getNodesMetoroCall(ctx context.Context, request model.GetAllNodesRequest) ([]byte, error) {
42 | requestBody, err := json.Marshal(request)
43 | if err != nil {
44 | return nil, fmt.Errorf("error marshaling nodes request: %v", err)
45 | }
46 | return utils.MakeMetoroAPIRequest("POST", "infrastructure/nodes", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
47 | }
48 |
--------------------------------------------------------------------------------
/tools/get_pods.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetPodsHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get pods for. e.g. if you want to get pods for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | ServiceName string `json:"serviceName" jsonschema:"description=The name of the service to get pods for. One of serviceName or nodeName is required"`
17 | NodeName string `json:"nodeName" jsonschema:"description=The name of the node to get pods for. One of serviceName or nodeName is required"`
18 | Environments []string `json:"environments" jsonschema:"description=The environments to get pods for. If empty, all environments will be used."`
19 | }
20 |
21 | func GetPodsHandler(ctx context.Context, arguments GetPodsHandlerArgs) (*mcpgolang.ToolResponse, error) {
22 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
23 | if err != nil {
24 | return nil, fmt.Errorf("error calculating time range: %v", err)
25 | }
26 |
27 | // One of serviceName or nodeName is required.
28 | if arguments.ServiceName == "" && arguments.NodeName == "" {
29 | return nil, fmt.Errorf("one of serviceName or nodeName is required")
30 | }
31 |
32 | request := model.GetPodsRequest{
33 | StartTime: startTime,
34 | EndTime: endTime,
35 | Environments: arguments.Environments,
36 | ServiceName: arguments.ServiceName,
37 | NodeName: arguments.NodeName,
38 | }
39 |
40 | jsonBody, err := json.Marshal(request)
41 | if err != nil {
42 | return nil, fmt.Errorf("error marshaling request: %v", err)
43 | }
44 |
45 | resp, err := utils.MakeMetoroAPIRequest("POST", "k8s/pods", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
46 | if err != nil {
47 | return nil, fmt.Errorf("error making Metoro call: %v", err)
48 | }
49 |
50 | response := []PodsResponse{}
51 | err = json.Unmarshal(resp, &response)
52 | if err != nil {
53 | return nil, fmt.Errorf("error unmarshaling response: %v", err)
54 | }
55 |
56 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprint(response))), nil
57 | }
58 |
59 | type PodsResponse struct {
60 | Name string `json:"name"`
61 | Environment string `json:"environment"`
62 | Status string `json:"status"`
63 | }
64 |
--------------------------------------------------------------------------------
/tools/get_profiles.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetProfileHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get the profiles data. e.g. if you want to get profiles for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | ServiceName string `json:"serviceName" jsonschema:"required,description=The name of the service to get profiles for"`
17 | ContainerNames []string `json:"containerNames" jsonschema:"description=The container names to get profiles for"`
18 | }
19 |
20 | func GetProfilesHandler(ctx context.Context, arguments GetProfileHandlerArgs) (*mcpgolang.ToolResponse, error) {
21 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
22 | if err != nil {
23 | return nil, fmt.Errorf("error calculating time range: %v", err)
24 | }
25 | request := model.GetProfileRequest{
26 | StartTime: startTime,
27 | EndTime: endTime,
28 | ServiceName: arguments.ServiceName,
29 | ContainerNames: arguments.ContainerNames,
30 | }
31 |
32 | body, err := getProfilesMetoroCall(ctx, request)
33 | if err != nil {
34 | return nil, fmt.Errorf("error getting profiles: %v", err)
35 | }
36 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
37 | }
38 |
39 | func getProfilesMetoroCall(ctx context.Context, request model.GetProfileRequest) ([]byte, error) {
40 | requestBody, err := json.Marshal(request)
41 | if err != nil {
42 | return nil, fmt.Errorf("error marshaling profiles request: %v", err)
43 | }
44 | return utils.MakeMetoroAPIRequest("POST", "profiles", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
45 | }
46 |
--------------------------------------------------------------------------------
/tools/get_service_graph.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/utils"
11 | )
12 |
13 | type GetServiceGraphHandlerArgs struct {
14 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get the service graph for. e.g. if you want to get the graph for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
15 | ServiceName string `json:"serviceName" jsonschema:"required,description=The name of the service to get the graph for"`
16 | Environments []string `json:"environments" jsonschema:"description=The environments to get the service graph for. If empty all environments will be used."`
17 | }
18 |
19 | type GetServiceGraphRequest struct {
20 | // If Environments is not empty, only services that are in the list will be included in the graph.
21 | // If Environments is empty, all services will be included in the graph.
22 | Environments []string `json:"environments"`
23 | // If InitialServices is not empty, only services that are in the list will be included in the graph.
24 | // If InitialServices is empty, all services will be included in the graph.
25 | InitialServices []string `json:"initialServices"`
26 | // If EndingServices is not empty, only services that are in the list will be included in the graph.
27 | // If EndingServices is empty, all services will be included in the graph.
28 | EndingServices []string `json:"endingServices"`
29 | // StartTime is the start time of the graph in seconds since epoch
30 | StartTime int64 `json:"startTime"`
31 | // EndTime is the end time of the graph in seconds since epoch
32 | EndTime int64 `json:"endTime"`
33 | // The filters to apply to the traces, so for example, if you want to get traces for a specific service
34 | // you can pass in a filter like {"service_name": ["microservice_a"]}
35 | Filters map[string][]string `json:"filters"`
36 | // ExcludeFilters are filters that should be excluded from the traces
37 | // For example, if you want to get traces for all services except microservice_a you can pass in
38 | // {"service_name": ["microservice_a"]}
39 | ExcludeFilters map[string][]string `json:"excludeFilters"`
40 | // Regexes are used to filter traces based on a regex inclusively
41 | Regexes []string `json:"regexes"`
42 | // ExcludeRegexes are used to filter traces based on a regex exclusively
43 | ExcludeRegexes []string `json:"excludeRegexes"`
44 | }
45 |
46 | func GetServiceGraphHandler(ctx context.Context, arguments GetServiceGraphHandlerArgs) (*mcpgolang.ToolResponse, error) {
47 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
48 | if err != nil {
49 | return nil, fmt.Errorf("error calculating time range: %v", err)
50 | }
51 |
52 | request := GetServiceGraphRequest{
53 | StartTime: startTime,
54 | EndTime: endTime,
55 | Environments: arguments.Environments,
56 | InitialServices: []string{arguments.ServiceName},
57 | EndingServices: []string{arguments.ServiceName},
58 | }
59 |
60 | body, err := getServiceGraphMetoroCall(ctx, request)
61 | if err != nil {
62 | return nil, fmt.Errorf("error getting service graph: %v", err)
63 | }
64 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
65 | }
66 |
67 | func getServiceGraphMetoroCall(ctx context.Context, request GetServiceGraphRequest) ([]byte, error) {
68 | requestBody, err := json.Marshal(request)
69 | if err != nil {
70 | return nil, fmt.Errorf("error marshaling service graph request: %v", err)
71 | }
72 | return utils.MakeMetoroAPIRequest("POST", "serviceGraph", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
73 | }
74 |
--------------------------------------------------------------------------------
/tools/get_service_summaries.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetServiceSummariesHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get service summaries for. e.g. if you want to get summaries for the last 5 minutes you would set time_period=5 and time_window=Minutes. Try to use a time period 1 hour or less. You can also set an absoulute time range by setting start_time and end_time"`
16 | Namespaces string `json:"namespace" jsonschema:"description=The namespace to get service summaries for. If empty all namespaces will be used."`
17 | Environments []string `json:"environments" jsonschema:"description=The environments to get service summaries for. If empty all environments will be used."`
18 | }
19 |
20 | func GetServiceSummariesHandler(ctx context.Context, arguments GetServiceSummariesHandlerArgs) (*mcpgolang.ToolResponse, error) {
21 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
22 | if err != nil {
23 | return nil, fmt.Errorf("error calculating time range: %v", err)
24 | }
25 | request := model.GetServiceSummariesRequest{
26 | StartTime: startTime,
27 | EndTime: endTime,
28 | Namespace: arguments.Namespaces,
29 | Environments: arguments.Environments,
30 | }
31 |
32 | body, err := getServiceSummariesMetoroCall(ctx, request)
33 | if err != nil {
34 | return nil, fmt.Errorf("error getting service summaries: %v", err)
35 | }
36 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
37 | }
38 |
39 | func getServiceSummariesMetoroCall(ctx context.Context, request model.GetServiceSummariesRequest) ([]byte, error) {
40 | requestBody, err := json.Marshal(request)
41 | if err != nil {
42 | return nil, fmt.Errorf("error marshaling service summaries request: %v", err)
43 | }
44 | return utils.MakeMetoroAPIRequest("POST", "serviceSummaries", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
45 | }
46 |
--------------------------------------------------------------------------------
/tools/get_services.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetServicesHandlerArgs struct{}
12 |
13 | func GetServicesHandler(ctx context.Context, arguments GetServicesHandlerArgs) (*mcpgolang.ToolResponse, error) {
14 | body, err := getServicesMetoroCall(ctx)
15 | if err != nil {
16 | return nil, fmt.Errorf("error getting services: %v", err)
17 | }
18 |
19 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
20 | }
21 |
22 | func getServicesMetoroCall(ctx context.Context) ([]byte, error) {
23 | return utils.MakeMetoroAPIRequest("GET", "services", nil, utils.GetAPIRequirementsFromRequest(ctx))
24 | }
25 |
--------------------------------------------------------------------------------
/tools/get_source_repository.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/utils"
11 | )
12 |
13 | type GetSourceRepositoryHandlerArgs struct {
14 | // Required: Service name to get the source repository for
15 | ServiceName string `json:"serviceName" jsonschema:"required,description=The name of the service to get the source repository for"`
16 |
17 | // Optional: Environment to filter by. If not provided, all environments are considered
18 | Environments []string `json:"environments" jsonschema:"description=List of environments to search for the service in. If empty all environments will be considered"`
19 |
20 | // Required: Time configuration for the query
21 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get the source repository information for. You can use relative time (e.g. last 5 minutes) or absolute time range."`
22 | }
23 |
24 | type GetSourceRepositoryRequest struct {
25 | ServiceName string `json:"serviceName"`
26 | Environments []string `json:"environments"`
27 | StartTime int64 `json:"startTime"`
28 | EndTime int64 `json:"endTime"`
29 | }
30 |
31 | type GetSourceRepositoryResponse struct {
32 | // The source repository URL/path found in the deployment
33 | Repository string `json:"repository"`
34 |
35 | // Whether a repository was found
36 | Found bool `json:"found"`
37 |
38 | // The environment where the repository information was found
39 | Environment string `json:"environment,omitempty"`
40 | }
41 |
42 | func GetSourceRepositoryHandler(ctx context.Context, arguments GetSourceRepositoryHandlerArgs) (*mcpgolang.ToolResponse, error) {
43 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
44 | if err != nil {
45 | return nil, fmt.Errorf("error calculating time range: %v", err)
46 | }
47 |
48 | body, err := getSourceRepositoryMetoroCall(ctx, arguments, startTime, endTime)
49 | if err != nil {
50 | return nil, fmt.Errorf("error getting source repository: %v", err)
51 | }
52 |
53 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
54 | }
55 |
56 | func getSourceRepositoryMetoroCall(ctx context.Context, args GetSourceRepositoryHandlerArgs, startTime, endTime int64) ([]byte, error) {
57 | req := GetSourceRepositoryRequest{
58 | ServiceName: args.ServiceName,
59 | Environments: args.Environments,
60 | StartTime: startTime,
61 | EndTime: endTime,
62 | }
63 |
64 | reqBody, err := json.Marshal(req)
65 | if err != nil {
66 | return nil, fmt.Errorf("error marshalling request: %v", err)
67 | }
68 |
69 | return utils.MakeMetoroAPIRequest("POST", "source/repository", bytes.NewBuffer(reqBody), utils.GetAPIRequirementsFromRequest(ctx))
70 | }
71 |
--------------------------------------------------------------------------------
/tools/get_trace_attribute_values.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetTraceAttributeValuesHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to use for getting the possible values for a trace attribute key. e.g. if you want to get possible trace attribute values for key x for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | Attribute string `json:"attribute" jsonschema:"required,description=The name of the attribute key to get the possible values for"`
17 | Filters map[string][]string `json:"filters" jsonschema:"description=The filters to apply before getting the possible values. For example if you want to get the possible values for attribute key service.name where the environment is X you would set the Filters as {environment: [X]}"`
18 | ExcludeFilters map[string][]string `json:"excludeFilters" jsonschema:"description=The exclude filters to exclude/eliminate possible values an attribute can take. Traces matching the exclude filters will not be returned. For example if you want the possible values for attribute key service.name where the attribute environment is not X then you would set the ExcludeFilters as {environment: [X]}"`
19 | Regexes []string `json:"regexes" jsonschema:"description=The regexes to apply to the trace endpoint. Only the attribute values (for a given attribute key) of trace endpoint that match these regexes will be returned. For example if you want the possible values for attribute key service.name where the trace endpoint contains the word 'get' you would set the regexes as ['get']"`
20 | ExcludeRegexes []string `json:"excludeRegexes" jsonschema:"description=The exclude regexes to apply to the trace endpoint. The attribute values (for a given attribute key) of trace endpoint that match these regexes will not be returned. For example if you want the possible values for attribute key service.name where the trace endpoint does not contain the word 'get' you would set the exclude regexes as ['get']"`
21 | Environments []string `json:"environments" jsonschema:"description=The environments to get traces from. If empty traces from all environments will be returned"`
22 | }
23 |
24 | func GetTraceAttributeValuesForIndividualAttributeHandler(ctx context.Context, arguments GetTraceAttributeValuesHandlerArgs) (*mcpgolang.ToolResponse, error) {
25 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
26 | if err != nil {
27 | return nil, fmt.Errorf("error calculating time range: %v", err)
28 | }
29 | request := model.GetSingleTraceSummaryRequest{
30 | TracesSummaryRequest: model.TracesSummaryRequest{
31 | StartTime: startTime,
32 | EndTime: endTime,
33 | Filters: arguments.Filters,
34 | ExcludeFilters: arguments.ExcludeFilters,
35 | Regexes: arguments.Regexes,
36 | ExcludeRegexes: arguments.ExcludeRegexes,
37 | Environments: arguments.Environments,
38 | },
39 | Attribute: arguments.Attribute,
40 | }
41 |
42 | jsonBody, err := json.Marshal(request)
43 | if err != nil {
44 | return nil, fmt.Errorf("error marshaling request: %v", err)
45 | }
46 |
47 | resp, err := utils.MakeMetoroAPIRequest("POST", "tracesSummaryIndividualAttribute", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
48 | if err != nil {
49 | return nil, fmt.Errorf("error making Metoro call: %v", err)
50 | }
51 |
52 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
53 | }
54 |
--------------------------------------------------------------------------------
/tools/get_trace_attributes.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "context"
5 | "fmt"
6 |
7 | mcpgolang "github.com/metoro-io/mcp-golang"
8 | "github.com/metoro-io/metoro-mcp-server/utils"
9 | )
10 |
11 | type GetTraceAttributesHandlerArgs struct{}
12 |
13 | func GetTraceAttributesHandler(ctx context.Context, arguments GetTraceAttributesHandlerArgs) (*mcpgolang.ToolResponse, error) {
14 | resp, err := utils.MakeMetoroAPIRequest("GET", "tracesSummaryAttributes", nil, utils.GetAPIRequirementsFromRequest(ctx))
15 | if err != nil {
16 | return nil, err
17 | }
18 |
19 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(resp)))), nil
20 | }
21 |
--------------------------------------------------------------------------------
/tools/get_trace_metric.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetTraceMetricHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get the trace timeseries data for. e.g. if you want to get the trace timeseries data for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time"`
16 | ServiceNames []string `json:"serviceNames" jsonschema:"description=Service names to return the trace timeseries data for"`
17 | Filters map[string][]string `json:"filters" jsonschema:"description=The filters to apply to the traces. it is a map of filter keys to array values where array values are ORed.e.g. key for service name is service.name"`
18 | ExcludeFilters map[string][]string `json:"excludeFilters" jsonschema:"description=The exclude filters to exclude/eliminate the traces. Traces matching the exclude traces will not be returned. it is a map of filter keys to array values where array values are ORed.e.g. key for service name is service.name"`
19 | Regexes []string `json:"regexes" jsonschema:"description=The regexes to apply to the trace's endpoints. Traces with endpoints matching regexes will be returned"`
20 | ExcludeRegexes []string `json:"excludeRegexes" jsonschema:"description=The regexes to exclude from the trace's endpoints. Traces with endpoints matching regexes will be excluded"`
21 | Splits []string `json:"splits" jsonschema:"description=The splits to apply to trace timeseries data. e.g. if you want to split the trace timeseries data by service name you would set splits as ['service.name']. This is useful for seeing a breakdown of the trace timeseries data by an attribute"`
22 | Functions []model.MetricFunction `json:"functions" jsonschema:"description=The functions to apply to the traces. Available functions are monotonicDifference which will calculate the difference between the current and previous value of the metric (negative values will be set to 0) and valueDifference which will calculate the difference between the current and previous value of the metric or MathExpression e.g. a / 60"`
23 | Aggregate string `json:"aggregate" jsonschema:"required,description=The aggregation to apply to the metrics. Possible values are: count / p50 / p90 / p95 / p99 / totalSize / responseSize / requestSize. The aggregation will be applied to every datapoint bucket. For example if the bucket size is 1 minute and the aggregation is count then the count of all datapoints in a minute will be returned"`
24 | Environments []string `json:"environments" jsonschema:"description=The environments to get traces from. If empty traces from all environments will be returned"`
25 | }
26 |
27 | func GetTraceMetricHandler(ctx context.Context, arguments GetTraceMetricHandlerArgs) (*mcpgolang.ToolResponse, error) {
28 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
29 | if err != nil {
30 | return nil, fmt.Errorf("error calculating time range: %v", err)
31 | }
32 | request := model.GetTraceMetricRequest{
33 | StartTime: startTime,
34 | EndTime: endTime,
35 | ServiceNames: arguments.ServiceNames,
36 | Filters: arguments.Filters,
37 | ExcludeFilters: arguments.ExcludeFilters,
38 | Regexes: arguments.Regexes,
39 | ExcludeRegexes: arguments.ExcludeRegexes,
40 | Splits: arguments.Splits,
41 | Functions: arguments.Functions,
42 | Aggregate: model.Aggregation(arguments.Aggregate),
43 | Environments: arguments.Environments,
44 | }
45 |
46 | body, err := getTraceMetricMetoroCall(ctx, request)
47 | if err != nil {
48 | return nil, fmt.Errorf("error getting trace metric: %v", err)
49 | }
50 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
51 | }
52 |
53 | func getTraceMetricMetoroCall(ctx context.Context, request model.GetTraceMetricRequest) ([]byte, error) {
54 | jsonBody, err := json.Marshal(request)
55 | if err != nil {
56 | return nil, fmt.Errorf("error marshaling request: %v", err)
57 | }
58 |
59 | resp, err := utils.MakeMetoroAPIRequest("POST", "traceMetric", bytes.NewBuffer(jsonBody), utils.GetAPIRequirementsFromRequest(ctx))
60 | if err != nil {
61 | return nil, fmt.Errorf("error making Metoro call: %v", err)
62 | }
63 |
64 | return resp, nil
65 | }
66 |
--------------------------------------------------------------------------------
/tools/get_trace_spans.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/utils"
11 | )
12 |
13 | type GetTraceSpansHandlerArgs struct {
14 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get trace spans for. e.g. if you want to get spans for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absolute time range by setting start_time and end_time"`
15 | TraceId string `json:"trace_id" jsonschema:"required,description=The traceId of the trace to get the associated spans. get_traces tool will return list of traceIds which should be used for this field."`
16 | Environments []string `json:"environments" jsonschema:"description=The environments to get the spans for. If empty all environments will be included"`
17 | }
18 |
19 | type GetSpansForTraceRequest struct {
20 | // Required: Start time of when to get the traces in seconds since epoch
21 | StartTime int64 `json:"startTime"`
22 | // Required: End time of when to get the traces in seconds since epoch
23 | EndTime int64 `json:"endTime"`
24 | // Required: The traceId of the trace to get the associated spans.
25 | TraceId string `json:"traceId"`
26 | // The environments to get the traces for. If empty, all environments will be included
27 | Environments []string `json:"environments"`
28 | ShouldReturnNonMetoroEpbfSpans bool `json:"shouldReturnNonMetoroEpbfSpans"`
29 | }
30 |
31 | func GetTraceSpansHandler(ctx context.Context, arguments GetTraceSpansHandlerArgs) (*mcpgolang.ToolResponse, error) {
32 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
33 | if err != nil {
34 | return nil, fmt.Errorf("error calculating time range: %v", err)
35 | }
36 |
37 | request := GetSpansForTraceRequest{
38 | StartTime: startTime,
39 | EndTime: endTime,
40 | TraceId: arguments.TraceId,
41 | Environments: arguments.Environments,
42 | ShouldReturnNonMetoroEpbfSpans: true,
43 | }
44 |
45 | body, err := getTraceSpansMetoroCall(ctx, request)
46 | if err != nil {
47 | return nil, fmt.Errorf("error getting trace spans: %v", err)
48 | }
49 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
50 | }
51 |
52 | func getTraceSpansMetoroCall(ctx context.Context, request GetSpansForTraceRequest) ([]byte, error) {
53 | requestBody, err := json.Marshal(request)
54 | if err != nil {
55 | return nil, fmt.Errorf("error marshaling trace spans request: %v", err)
56 | }
57 | return utils.MakeMetoroAPIRequest("POST", "spans", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
58 | }
59 |
--------------------------------------------------------------------------------
/tools/get_traces.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | import (
4 | "bytes"
5 | "context"
6 | "encoding/json"
7 | "fmt"
8 |
9 | mcpgolang "github.com/metoro-io/mcp-golang"
10 | "github.com/metoro-io/metoro-mcp-server/model"
11 | "github.com/metoro-io/metoro-mcp-server/utils"
12 | )
13 |
14 | type GetTracesHandlerArgs struct {
15 | TimeConfig utils.TimeConfig `json:"time_config" jsonschema:"required,description=The time period to get traces for. e.g. if you want to get traces for the last 5 minutes you would set time_period=5 and time_window=Minutes. You can also set an absoulute time range by setting start_time and end_time. Try to use a time period 1 hour or less unless its requested."`
16 | Filters map[string][]string `json:"filters" jsonschema:"description=Filters to apply to the traces. Only the traces that match these filters will be returned. You have to get the possible filter keys from the get_attribute_keys tool and possible values of a filter key from the get_attribute_values tool. DO NOT GUESS THE FILTER KEYS OR VALUES. Multiple filter keys are ANDed together and values for a filter key are ORed together"`
17 | ExcludeFilters map[string][]string `json:"excludeFilters" jsonschema:"description=The exclude filters to exclude/eliminate the traces. Traces matching the exclude traces will not be returned. You have to get the possible exclude filter keys from the get_attribute_keys tool and possible value for the key from the get_attribute_values tool. DO NOT GUESS THE FILTER KEYS OR VALUES. Multiple keys are ORed together and values for a filter key are ANDed together"`
18 | }
19 |
20 | func GetTracesHandler(ctx context.Context, arguments GetTracesHandlerArgs) (*mcpgolang.ToolResponse, error) {
21 | startTime, endTime, err := utils.CalculateTimeRange(arguments.TimeConfig)
22 | if err != nil {
23 | return nil, fmt.Errorf("error calculating time range: %v", err)
24 | }
25 |
26 | err = CheckAttributes(ctx, model.Trace, arguments.Filters, arguments.ExcludeFilters, []string{}, nil)
27 | if err != nil {
28 | return nil, err
29 | }
30 |
31 | request := model.GetTracesRequest{
32 | StartTime: startTime,
33 | EndTime: endTime,
34 | Filters: arguments.Filters,
35 | ExcludeFilters: arguments.ExcludeFilters,
36 | }
37 |
38 | body, err := getTracesMetoroCall(ctx, request)
39 | if err != nil {
40 | return nil, fmt.Errorf("error getting traces: %v", err)
41 | }
42 | return mcpgolang.NewToolResponse(mcpgolang.NewTextContent(fmt.Sprintf("%s", string(body)))), nil
43 | }
44 |
45 | func getTracesMetoroCall(ctx context.Context, request model.GetTracesRequest) ([]byte, error) {
46 | requestBody, err := json.Marshal(request)
47 | if err != nil {
48 | return nil, fmt.Errorf("error marshaling traces request: %v", err)
49 | }
50 | return utils.MakeMetoroAPIRequest("POST", "traces", bytes.NewBuffer(requestBody), utils.GetAPIRequirementsFromRequest(ctx))
51 | }
52 |
--------------------------------------------------------------------------------
/tools/tools.go:
--------------------------------------------------------------------------------
1 | package tools
2 |
3 | type MetoroTools struct {
4 | Name string
5 | Description string
6 | Handler any
7 | }
8 |
9 | var MetoroToolsList = []MetoroTools{
10 | {
11 | Name: "get_environments",
12 | Description: "Get Kubernetes environments/clusters. This tool is useful for listing the kubernetes environments/clusters that are monitored by Metoro.",
13 | Handler: GetEnvironmentsHandler,
14 | },
15 | {
16 | Name: "get_services",
17 | Description: "Get services running in your Kubernetes cluster. Metoro treats the following Kubernetes resources as a 'service': Deployment, StatefulSet, DaemonSet. This tool is useful for listing the services/workloads running in your Kubernetes cluster.",
18 | Handler: GetServicesHandler,
19 | },
20 | {
21 | Name: "get_namespaces",
22 | Description: "Get namespaces in your Kubernetes cluster. This tool is useful for listing the namespaces in your Kubernetes cluster.",
23 | Handler: GetNamespacesHandler,
24 | },
25 | {
26 | Name: "get_logs",
27 | Description: `Get logs from all or specific services/hosts/pods. Results are limited to 100 logs lines. Before using this you MUST first call get_attribute_keys and get_attribute_values to get the possible log attribute keys and values which can be used as Filter/ExcludeFilter keys.`,
28 | Handler: GetLogsHandler,
29 | },
30 | {
31 | Name: "get_traces",
32 | Description: `Get list of traces from your cluster. Results are limited to 100 traces so try to use filters to narrow down what you are looking for.
33 | Prior to using this tool, YOU MUST first call get_attribute_keys and subsequently get_attribute_values to get the possible trace attribute keys and values which can be used as Filter/ExcludeFilter keys.
34 | Use this tool when you are interested in the trace attributes to get more information to answer why/what. If you want more details about a specific trace use get_trace_spans to see individual span details.
35 | If you would like to check existence of traces use get_timeseries_data tool with type=trace to get count/p50/p90/p95/p99 of traces instead of using get_traces tool.
36 | After calling get traces you should normally call get_trace_spans to get the spans associated with the traceId you are interested in.
37 | `,
38 | Handler: GetTracesHandler,
39 | },
40 | {
41 | Name: "get_trace_spans",
42 | Description: `Get the spans associated with a specific traceId. This allows you to view the entire trace with all its spans in a tree like structure. You should basically always use this after calling get_traces tool to get the traceId you are interested in. This tool gives you all spans in a trace.`,
43 | Handler: GetTraceSpansHandler,
44 | },
45 | {
46 | Name: "get_timeseries_data",
47 | Description: `Get one or more timeseries data for a metric or traces or logs or kubernetes resources. This tool is useful for understanding how the underlying type of data (specific/metric/trace/kubernetes resources/logs) change over time. You can also apply formulas to combine timeseries to calculate rates or ratios or differences etc. How to use this tool:
48 | First you need the type of timeseries data you are requesting for. This can be one of metric or traces or logs or kubernetes resources. If it is metrics then you HAVE TO call the get_metric_names tool to get the available metric names which can be used as MetricName argument for this tool.
49 | Then YOU HAVE TO call get_attribute_keys tool to retrieve the available attribute keys and get_attribute_values to retrieve values you are interested in to use in Filter/ExcludeFilter keys for this tool.
50 | You can also use Splits argument to group/split the metric data by the given metric attribute keys. Only use the attribute keys and values that are available for the MetricName that are returned from get_attribute_keys and get_attribute_values tools. If you are not getting proper results back then you might have forgotten to set the correct attribute keys and values. Try again with the correct attribute keys and values you get from get_attribute_values.
51 | Metrics of type counter (or with _total suffix) are cumulative metrics but Metoro querying engine already accounts for rate differences when returning the value so you don't need to calculate the rate/monotonic difference yourself. You can just query those metrics as they are without extra functions. If you are in doubt use the get_metric_metadata tool to get more information (description type unit) about the metric and how to use it.
52 | `,
53 | Handler: GetMultiMetricHandler,
54 | },
55 | {
56 | Name: "get_attribute_keys",
57 | Description: `Get the possible attribute keys for a specific type of data. This tool is useful for understanding the possible attribute keys that can be used for filtering the data. How to use this tool:
58 | First you need the type of data you are requesting for. This can be one of metric or traces or logs or kubernetes resources.
59 | Then you can call this tool to get the possible attribute keys for the given type of data.`,
60 | Handler: GetAttributeKeysHandler,
61 | },
62 | {
63 | Name: "get_attribute_values",
64 | Description: `"Get the possible values of an attribute key for a given type of data which can be one of metric trace logs or kubernetes_resource. This can be used as a value for a filtering key for filtering data. How to use this tool:
65 | First you need the type of data you are requesting for. This can be one of metric or traces or logs or kubernetes resources. Then you need the attribute keys for the given type of data. You can use get_attribute_keys tool to get the available attribute keys for the given type of data.
66 | Then you can call this tool to get the possible values for a given attribute key for the given type of data. If you want to get the possible values for a metric attribute key you can use the get_metric_names tool to get the available metric names which can be used as MetricName argument for this tool and then use get_attribute_keys tool to get the available attribute keys and get_attribute_values to get values for the key which can be used as Filter/ExcludeFilter keys for`,
67 | Handler: GetAttributeValuesHandler,
68 | },
69 | {
70 | Name: "get_profiles",
71 | Description: "Get cpu profiles of your services running in your Kubernetes cluster. This tool is useful for answering performance related questions for a specific service. It provides information about which functions taking time in the service.",
72 | Handler: GetProfilesHandler,
73 | },
74 | {
75 | Name: "get_k8s_events",
76 | Description: `Get the Kubernetes events from your clusters. Kubernetes events are useful for understanding what is happening with regards to your Kubernetes resources.
77 | They are emitted by the Kubernetes API server when there is a change in the state of the cluster. How to use this tool:
78 | First use get_k8s_events_attributes tool to retrieve the available Kubernetes event attribute keys which can be used as Filter/ExcludeFilter keys for this tool.
79 | Then use get_k8s_event_attribute_values_for_individual_attribute tool to get the possible values a Kubernetes event attribute key can be for filtering Kubernetes events.
80 | And then you can call this tool (get_k8s_events) to get the specific events you are looking for. e.g. Filter use case: get_k8s_events with filters: {key: [value]} for including specific Kubernetes events.`,
81 | Handler: GetK8sEventsHandler,
82 | },
83 | {
84 | Name: "get_k8s_events_attributes",
85 | Description: "Get possible attribute keys for Kubernetes events which can be used for filtering them.",
86 | Handler: GetK8sEventsAttributesHandler,
87 | },
88 | {
89 | Name: "get_k8s_event_attribute_values_for_individual_attribute",
90 | Description: "Get possible attribute values for a specific Kubernetes event attribute key. E.g. EventType attribute key might have values like Normal Warning etc.",
91 | Handler: GetK8sEventAttributeValuesForIndividualAttributeHandler,
92 | },
93 | {
94 | Name: "get_metric_names",
95 | Description: "Get available metric names to query. These metric names can be used as MetricName argument for get_metric get_metric_metadata and get_timeseries_data and get_attribute_keys tools.",
96 | Handler: GetMetricNamesHandler,
97 | },
98 | {
99 | Name: "get_metric_metadata",
100 | Description: "Get metric description and type and unit for a metric. This tool can be used to get detailed information about a metric including its type unit and description. Use this tool after getting the metric name that you are interested in from the get_metric_names tool and before calling the get_timeseries_data tool to understand the metric better.",
101 | Handler: GetMetricMetadata,
102 | },
103 | //{
104 | // Name: "get_pods",
105 | // Description: "Get the list of pods that are running in your cluster. This tool is useful for getting the name of the pods. You must provide either a ServiceName to get pods for a specific service or a NodeName to get pods running on a specific node.",
106 | // Handler: GetPodsHandler,
107 | //},
108 | {
109 | Name: "get_service_yaml",
110 | Description: "Returns environment and YAML of a kubernetes resource/service. This tool is useful for understanding the YAML configuration of a service.",
111 | Handler: GetK8sServiceInformationHandler,
112 | },
113 | {
114 | Name: "get_nodes",
115 | Description: "Get the nodes that are running in your cluster. To use this tool first call get_node_attributes to get the possible node attribute keys and values which can be used for filtering nodes.",
116 | Handler: GetNodesHandler,
117 | },
118 | {
119 | Name: "get_node_attributes",
120 | Description: "Get possible node attribute keys and values which can be used for filtering nodes.",
121 | Handler: GetNodeAttributesHandler,
122 | },
123 | {
124 | Name: "get_node_info",
125 | Description: "Get detailed node information about a specific node. This tool provides information about the node's capacity allocatable resources and usage yaml node type OS and Kernel information.",
126 | Handler: GetNodeInfoHandler,
127 | },
128 | {
129 | Name: "get_service_summaries",
130 | Description: "Get summaries of services/workloads running in your Kubernetes cluster. The summary includes the number of requests errors (5xx and 4xx) P50 p95 p99 latencies. This tool is useful for understanding the performance of your services at a high level for a given relative or abosulute time range.",
131 | Handler: GetServiceSummariesHandler,
132 | },
133 | {
134 | Name: "get_alerts",
135 | Description: "Get list of alerts from your Kubernetes cluster. These alerts are configured by the user in Metoro therefore it may not have full coverage for all the issues that might occur in the cluster.",
136 | Handler: GetAlertsHandler,
137 | },
138 | {
139 | Name: "get_alert_fires",
140 | Description: "Get list of alert fires from your Kubernetes cluster. Alert fires are the instances when an alert is triggered. This tool provides information about the alert name the time it was triggered the time it recovered the environment and the service name (if available) and the alert trigger message.",
141 | Handler: GetAlertFiresHandler,
142 | },
143 | {
144 | Name: "create_dashboard",
145 | Description: `Create a dashboard with the described metrics. This tool is useful for creating a dashboard with the metrics you are interested in.
146 | How to use this tool:
147 | First use get_metric_names tool to retrieve the available metric names which can be used as MetricName argument for this tool and then use get_attribute_keys tool to retrieve the available attribute keys and get_attribute_values for getting the values for the attribute key that you are interested in to use in Filter/ExcludeFilter keys or Splits argument for MetricChartWidget argument for this tool.
148 | You can also use Splits argument to group the metric data by the given metric attribute keys. Only use the attribute keys and values that are available for the MetricName that are returned from get_attribute_keys and get_attribute_values tools.`,
149 | Handler: CreateDashboardHandler,
150 | },
151 | {
152 | Name: "get_source_repository",
153 | Description: "Get the source repository URL/path for a specific service. This tool is useful for finding where the code for a service is stored. You need to provide the service name time range and optionally specific environments to search in.",
154 | Handler: GetSourceRepositoryHandler,
155 | },
156 | {
157 | Name: "get_service_graph",
158 | Description: "Get the service graph showing which services make calls to a given service and which services the given service makes calls to. This tool is useful for understanding service dependencies and call patterns.",
159 | Handler: GetServiceGraphHandler,
160 | },
161 | }
162 |
--------------------------------------------------------------------------------
/utils/request_utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "net/http"
9 | "os"
10 |
11 | "github.com/gin-gonic/gin"
12 | )
13 |
14 | const METORO_API_URL_ENV_VAR = "METORO_API_URL"
15 | const METORO_AUTH_TOKEN_ENV_VAR = "METORO_AUTH_TOKEN"
16 |
17 | type APIRequirements struct {
18 | authHeader string
19 | metoroUrl string
20 | }
21 |
22 | func GetAPIRequirementsFromRequest(ctx context.Context) *APIRequirements {
23 | c := ctx.Value("ginContext")
24 | if c == nil {
25 | return nil
26 | }
27 | ginContext, ok := c.(*gin.Context)
28 | if !ok {
29 | return nil
30 | }
31 |
32 | if ginContext.Request.Header.Get("Authorization") != "" {
33 | return &APIRequirements{
34 | authHeader: ginContext.Request.Header.Get("Authorization"),
35 | metoroUrl: "http://localhost:8080",
36 | }
37 | }
38 | return nil
39 | }
40 |
41 | // makeMetoroAPIRequest makes an HTTP request to the Metoro API with the given method, endpoint, and body.
42 | // It handles authentication and common error cases.
43 | func MakeMetoroAPIRequest(method, endpoint string, body io.Reader, apiRequirements *APIRequirements) ([]byte, error) {
44 | // Create a new HTTP client
45 | client := &http.Client{}
46 | if apiRequirements == nil {
47 | apiRequirements = &APIRequirements{
48 | authHeader: "Bearer " + os.Getenv(METORO_AUTH_TOKEN_ENV_VAR),
49 | metoroUrl: os.Getenv(METORO_API_URL_ENV_VAR),
50 | }
51 | }
52 |
53 | // Create a new request
54 | req, err := http.NewRequest(method, fmt.Sprintf("%s/api/v1/%s", apiRequirements.metoroUrl, endpoint), body)
55 | if err != nil {
56 | return nil, fmt.Errorf("error creating request: %v", err)
57 | }
58 |
59 | // Add the Authorization header
60 | req.Header.Add("Authorization", apiRequirements.authHeader)
61 |
62 | // Send the request
63 | resp, err := client.Do(req)
64 | if err != nil {
65 | return nil, fmt.Errorf("error sending request: %v", err)
66 | }
67 | defer resp.Body.Close()
68 |
69 | // Read the response body
70 | responseBody, err := ioutil.ReadAll(resp.Body)
71 | if err != nil {
72 | return nil, fmt.Errorf("error reading response body: %v", err)
73 | }
74 |
75 | // Check the response status code
76 | if resp.StatusCode != http.StatusOK {
77 | return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
78 | }
79 |
80 | return responseBody, nil
81 | }
82 |
--------------------------------------------------------------------------------
/utils/time_utils.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "time"
7 | )
8 |
9 | // TimeWindow represents supported time window units
10 | type TimeWindow string
11 |
12 | const (
13 | Minutes TimeWindow = "Minutes"
14 | Hours TimeWindow = "Hours"
15 | Days TimeWindow = "Days"
16 | )
17 |
18 | // TimeRangeType indicates whether the time range is relative or absolute
19 | type TimeRangeType string
20 |
21 | const (
22 | RelativeTimeRange TimeRangeType = "relative"
23 | AbsoluteTimeRange TimeRangeType = "absolute"
24 | )
25 |
26 | // TimeConfig holds the configuration for time range calculation
27 | type TimeConfig struct {
28 | // Type of time range (relative or absolute)
29 | Type TimeRangeType `json:"type" jsonschema:"required,enum=relative,enum=absolute,description=Type of time range. Must be either 'relative' or 'absolute'"`
30 |
31 | // Fields for relative time range
32 | TimePeriod *int `json:"time_period,omitempty" jsonschema:"description=For relative time range: the number of time units to look back"`
33 | TimeWindow *TimeWindow `json:"time_window,omitempty" jsonschema:"description=For relative time range: the unit of time (Minutes, Hours, Days)"`
34 |
35 | // Fields for absolute time range
36 | StartTime *string `json:"start_time,omitempty" jsonschema:"description=For absolute time range: start time in RFC3339 format (e.g., '2024-12-12T14:27:22Z')"`
37 | EndTime *string `json:"end_time,omitempty" jsonschema:"description=For absolute time range: end time in RFC3339 format (e.g., '2024-12-12T14:27:22Z')"`
38 | }
39 |
40 | // CalculateTimeRange returns start and end timestamps based on the time configuration
41 | func CalculateTimeRange(config TimeConfig) (startTime, endTime int64, err error) {
42 | now := time.Now()
43 |
44 | switch config.Type {
45 | case RelativeTimeRange:
46 | if config.TimePeriod == nil || config.TimeWindow == nil {
47 | return 0, 0, fmt.Errorf("time_period and time_window are required for relative time range")
48 | }
49 |
50 | var duration time.Duration
51 | window := strings.ToLower(string(*config.TimeWindow))
52 |
53 | switch window {
54 | case "minutes", "minute", "min", "mins":
55 | duration = time.Duration(*config.TimePeriod) * time.Minute
56 | case "hours", "hour", "hr", "hrs":
57 | duration = time.Duration(*config.TimePeriod) * time.Hour
58 | case "days", "day":
59 | duration = time.Duration(*config.TimePeriod) * 24 * time.Hour
60 | default:
61 | return 0, 0, fmt.Errorf("invalid time window: %s", *config.TimeWindow)
62 | }
63 |
64 | startTimeObj := now.Add(-duration)
65 | return startTimeObj.Unix(), now.Unix(), nil
66 |
67 | case AbsoluteTimeRange:
68 | if config.StartTime == nil || config.EndTime == nil {
69 | return 0, 0, fmt.Errorf("start_time and end_time are required for absolute time range")
70 | }
71 |
72 | startTimeObj, err := time.Parse(time.RFC3339, *config.StartTime)
73 | if err != nil {
74 | return 0, 0, fmt.Errorf("invalid start_time format: %v", err)
75 | }
76 |
77 | endTimeObj, err := time.Parse(time.RFC3339, *config.EndTime)
78 | if err != nil {
79 | return 0, 0, fmt.Errorf("invalid end_time format: %v", err)
80 | }
81 |
82 | if endTimeObj.Before(startTimeObj) {
83 | return 0, 0, fmt.Errorf("end_time cannot be before start_time")
84 | }
85 |
86 | return startTimeObj.Unix(), endTimeObj.Unix(), nil
87 |
88 | default:
89 | return 0, 0, fmt.Errorf("invalid time range type: %s", config.Type)
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/utils/time_utils_test.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "testing"
5 | "time"
6 | "strings"
7 | )
8 |
9 | func TestCalculateTimeRange(t *testing.T) {
10 | // Helper function to create pointers
11 | strPtr := func(s string) *string { return &s }
12 | intPtr := func(i int) *int { return &i }
13 | timeWindowPtr := func(tw TimeWindow) *TimeWindow { return &tw }
14 |
15 | tests := []struct {
16 | name string
17 | config TimeConfig
18 | wantPeriod *time.Duration // Expected time difference between start and end for relative time
19 | wantStartTime *time.Time // Expected exact start time for absolute time
20 | wantEndTime *time.Time // Expected exact end time for absolute time
21 | wantErr bool
22 | errMsg string
23 | }{
24 | // Relative time tests
25 | {
26 | name: "relative - 5 minutes",
27 | config: TimeConfig{
28 | Type: RelativeTimeRange,
29 | TimePeriod: intPtr(5),
30 | TimeWindow: timeWindowPtr(Minutes),
31 | },
32 | wantPeriod: func() *time.Duration {
33 | d := 5 * time.Minute
34 | return &d
35 | }(),
36 | },
37 | {
38 | name: "relative - 2 hours",
39 | config: TimeConfig{
40 | Type: RelativeTimeRange,
41 | TimePeriod: intPtr(2),
42 | TimeWindow: timeWindowPtr(Hours),
43 | },
44 | wantPeriod: func() *time.Duration {
45 | d := 2 * time.Hour
46 | return &d
47 | }(),
48 | },
49 | {
50 | name: "relative - missing time period",
51 | config: TimeConfig{
52 | Type: RelativeTimeRange,
53 | TimeWindow: timeWindowPtr(Minutes),
54 | },
55 | wantErr: true,
56 | errMsg: "time_period and time_window are required for relative time range",
57 | },
58 | {
59 | name: "relative - invalid time window",
60 | config: TimeConfig{
61 | Type: RelativeTimeRange,
62 | TimePeriod: intPtr(5),
63 | TimeWindow: timeWindowPtr("invalid"),
64 | },
65 | wantErr: true,
66 | errMsg: "invalid time window: invalid",
67 | },
68 |
69 | // Absolute time tests
70 | {
71 | name: "absolute - valid time range",
72 | config: TimeConfig{
73 | Type: AbsoluteTimeRange,
74 | StartTime: strPtr("2024-12-12T14:00:00Z"),
75 | EndTime: strPtr("2024-12-12T15:00:00Z"),
76 | },
77 | wantStartTime: func() *time.Time {
78 | t, _ := time.Parse(time.RFC3339, "2024-12-12T14:00:00Z")
79 | return &t
80 | }(),
81 | wantEndTime: func() *time.Time {
82 | t, _ := time.Parse(time.RFC3339, "2024-12-12T15:00:00Z")
83 | return &t
84 | }(),
85 | },
86 | {
87 | name: "absolute - missing start time",
88 | config: TimeConfig{
89 | Type: AbsoluteTimeRange,
90 | EndTime: strPtr("2024-12-12T15:00:00Z"),
91 | },
92 | wantErr: true,
93 | errMsg: "start_time and end_time are required for absolute time range",
94 | },
95 | {
96 | name: "absolute - invalid start time format",
97 | config: TimeConfig{
98 | Type: AbsoluteTimeRange,
99 | StartTime: strPtr("invalid"),
100 | EndTime: strPtr("2024-12-12T15:00:00Z"),
101 | },
102 | wantErr: true,
103 | errMsg: "invalid start_time format",
104 | },
105 | {
106 | name: "absolute - end time before start time",
107 | config: TimeConfig{
108 | Type: AbsoluteTimeRange,
109 | StartTime: strPtr("2024-12-12T15:00:00Z"),
110 | EndTime: strPtr("2024-12-12T14:00:00Z"),
111 | },
112 | wantErr: true,
113 | errMsg: "end_time cannot be before start_time",
114 | },
115 |
116 | // Invalid type test
117 | {
118 | name: "invalid time range type",
119 | config: TimeConfig{
120 | Type: "invalid",
121 | },
122 | wantErr: true,
123 | errMsg: "invalid time range type: invalid",
124 | },
125 | }
126 |
127 | for _, tt := range tests {
128 | t.Run(tt.name, func(t *testing.T) {
129 | startTime, endTime, err := CalculateTimeRange(tt.config)
130 |
131 | // Check error cases
132 | if tt.wantErr {
133 | if err == nil {
134 | t.Errorf("CalculateTimeRange() error = nil, wantErr %v", tt.wantErr)
135 | return
136 | }
137 | if tt.errMsg != "" && !strings.Contains(err.Error(), tt.errMsg) {
138 | t.Errorf("CalculateTimeRange() error = %v, want error containing %v", err, tt.errMsg)
139 | }
140 | return
141 | }
142 |
143 | if err != nil {
144 | t.Errorf("CalculateTimeRange() unexpected error = %v", err)
145 | return
146 | }
147 |
148 | // For relative time tests
149 | if tt.wantPeriod != nil {
150 | startTimeObj := time.Unix(startTime, 0)
151 | endTimeObj := time.Unix(endTime, 0)
152 | gotPeriod := endTimeObj.Sub(startTimeObj)
153 | if gotPeriod != *tt.wantPeriod {
154 | t.Errorf("CalculateTimeRange() time period = %v, want %v", gotPeriod, *tt.wantPeriod)
155 | }
156 |
157 | // Check if endTime is approximately now (within 1 second tolerance)
158 | nowUnix := time.Now().Unix()
159 | if diff := abs(endTime - nowUnix); diff > 1 {
160 | t.Errorf("CalculateTimeRange() endTime is not close enough to current time. diff = %v seconds", diff)
161 | }
162 | }
163 |
164 | // For absolute time tests
165 | if tt.wantStartTime != nil && tt.wantEndTime != nil {
166 | if startTime != tt.wantStartTime.Unix() {
167 | t.Errorf("CalculateTimeRange() startTime = %v, want %v", startTime, tt.wantStartTime.Unix())
168 | }
169 | if endTime != tt.wantEndTime.Unix() {
170 | t.Errorf("CalculateTimeRange() endTime = %v, want %v", endTime, tt.wantEndTime.Unix())
171 | }
172 | }
173 | })
174 | }
175 | }
176 |
177 | func abs(x int64) int64 {
178 | if x < 0 {
179 | return -x
180 | }
181 | return x
182 | }
183 |
--------------------------------------------------------------------------------