├── .github └── workflows │ ├── tinybird-cd.yml │ └── tinybird-ci.yml ├── .gitignore ├── LICENSE ├── README.md ├── bluesky-demo ├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── next.config.ts ├── package-lock.json ├── package.json ├── pnpm-lock.yaml ├── postcss.config.mjs ├── src │ ├── app │ │ ├── api │ │ │ └── token │ │ │ │ └── route.ts │ │ ├── favicon.ico │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components │ │ └── ui │ │ │ ├── button.tsx │ │ │ └── input.tsx │ └── lib │ │ └── utils.ts ├── tailwind.config.ts └── tsconfig.json ├── dashboard-template.png ├── mcp-server-analytics ├── README.md ├── TEMPLATE.md ├── dashboard.png ├── mcp-server-metrics-v1.json ├── mcp-server-metrics-with-logs-v1.json ├── prometheus.png ├── region.png └── tinybird │ ├── .tinyenv │ ├── datasources │ ├── fixtures │ │ └── .gitkeep │ ├── mcp_logs.datasource │ ├── mcp_logs_python.datasource │ ├── mcp_monitoring.datasource │ ├── mv_count_mcp_errors_by_tool.datasource │ ├── mv_count_mcp_logs_by_prompt.datasource │ ├── mv_count_mcp_logs_by_tool.datasource │ ├── mv_uniq_mcp_sessions_by_app.datasource │ └── prompts.datasource │ ├── pipes │ ├── api_count_errors.pipe │ ├── api_count_errors_5m.pipe │ ├── api_count_requests.pipe │ ├── api_count_requests_5m.pipe │ ├── api_count_uniq_sessions.pipe │ ├── api_count_uniq_sessions_5m.pipe │ ├── api_logs.pipe │ ├── api_prometheus.pipe │ ├── mv_count_mcp_errors_by_tool_pipe.pipe │ ├── mv_count_mcp_logs_by_tool_pipe.pipe │ ├── mv_mcp_logs_pipe.pipe │ ├── mv_mcp_logs_python_pipe.pipe │ └── mv_uniq_mcp_sessions_by_app_pipe.pipe │ ├── requirements.txt │ ├── scripts │ ├── append_fixtures.sh │ └── exec_test.sh │ └── tests │ └── .gitkeep ├── pyproject.toml ├── src └── mcp_tinybird │ ├── __init__.py │ ├── __main__.py │ ├── run_sse.py │ ├── run_stdio.py │ ├── server.py │ ├── sse.py │ ├── stdio.py │ └── tb.py └── uv.lock /.github/workflows/tinybird-cd.yml: -------------------------------------------------------------------------------- 1 | 2 | ################################################## 3 | ### Visit https://github.com/tinybirdco/ci ### 4 | ### for more details or custom CI/CD ### 5 | ################################################## 6 | 7 | name: Tinybird - CD Workflow 8 | 9 | on: 10 | workflow_dispatch: 11 | push: 12 | paths: 13 | - 'mcp-server-analytics/tinybird/**' 14 | branches: 15 | - main 16 | jobs: 17 | cd: 18 | uses: tinybirdco/ci/.github/workflows/cd.yml@v4.1.0 19 | with: 20 | data_project_dir: ./mcp-server-analytics/tinybird 21 | secrets: 22 | tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }} 23 | tb_host: https://api.tinybird.co 24 | -------------------------------------------------------------------------------- /.github/workflows/tinybird-ci.yml: -------------------------------------------------------------------------------- 1 | 2 | ################################################## 3 | ### Visit https://github.com/tinybirdco/ci ### 4 | ### for more details or custom CI/CD ### 5 | ################################################## 6 | 7 | name: Tinybird - CI Workflow 8 | 9 | on: 10 | workflow_dispatch: 11 | pull_request: 12 | paths: 13 | - 'mcp-server-analytics/tinybird/**' 14 | branches: 15 | - main 16 | types: [opened, reopened, labeled, unlabeled, synchronize, closed] 17 | 18 | concurrency: ${{ github.workflow }}-${{ github.event.pull_request.number }} 19 | 20 | jobs: 21 | ci: 22 | uses: tinybirdco/ci/.github/workflows/ci.yml@v4.1.0 23 | with: 24 | data_project_dir: ./mcp-server-analytics/tinybird 25 | secrets: 26 | tb_admin_token: ${{ secrets.TB_ADMIN_TOKEN }} 27 | tb_host: https://api.tinybird.co 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python-generated files 2 | .DS_Store 3 | __pycache__/ 4 | *.py[oc] 5 | build/ 6 | dist/ 7 | wheels/ 8 | *.egg-info 9 | 10 | # Virtual environments 11 | .venv 12 | .env 13 | .python-version 14 | .tinyb 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tinybird MCP server 2 | 3 | [![smithery badge](https://smithery.ai/badge/mcp-tinybird)](https://smithery.ai/server/@tinybirdco/mcp-tinybird) 4 | 5 | An MCP server to interact with a Tinybird Workspace from any MCP client. 6 | 7 | Tinybird server MCP server 8 | 9 | ## Features 10 | 11 | - Query Tinybird Data Sources using the Tinybird Query API 12 | - Get the result of existing Tinybird API Endpoints with HTTP requests 13 | - Push Datafiles 14 | 15 | It supports both SSE and STDIO modes. 16 | 17 | ## Usage examples 18 | 19 | - [Bluesky metrics](https://bsky.app/profile/alasdairb.com/post/3lbx2mq5urk22) ([Claude transcript](https://www.tinybird.co/blog-posts/claude-analyze-bluesky-data-tinybird-mcp-server)) 20 | - [Web analytics starter kit metrics](https://github.com/tinybirdco/web-analytics-starter-kit) ([video](https://x.com/alrocar/status/1861849648882688341)] 21 | 22 | ## Setup 23 | 24 | ### Installation 25 | 26 | #### Using MCP package managers 27 | 28 | **Smithery** 29 | 30 | To install Tinybird MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/protocol/mcp-tinybird): 31 | 32 | ```bash 33 | npx @smithery/cli install @tinybirdco/mcp-tinybird --client claude 34 | ``` 35 | 36 | **mcp-get** 37 | 38 | You can install the Tinybird MCP server using [mcp-get](https://github.com/michaellatman/mcp-get): 39 | 40 | ```bash 41 | npx @michaellatman/mcp-get@latest install mcp-tinybird 42 | ``` 43 | 44 | ### Prerequisites 45 | 46 | MCP is still very new and evolving, we recommend following the [MCP documentation](https://modelcontextprotocol.io/quickstart#prerequisites) to get the MCP basics up and running. 47 | 48 | You'll need: 49 | - [Tinybird Account & Workspace](https://www.tinybird.co/) 50 | - [Claude Desktop](https://claude.ai/) 51 | - [uv](https://docs.astral.sh/uv/getting-started/installation/) 52 | 53 | ### Configuration 54 | 55 | #### 1. Configure Claude Desktop 56 | 57 | Create the following file depending on your OS: 58 | 59 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 60 | 61 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json` 62 | 63 | Paste this template in the file and replace `` and `` with your Tinybird API URL and Admin Token: 64 | 65 | ```json 66 | { 67 | "mcpServers": { 68 | "mcp-tinybird": { 69 | "command": "uvx", 70 | "args": [ 71 | "mcp-tinybird", 72 | "stdio" 73 | ], 74 | "env": { 75 | "TB_API_URL": "", 76 | "TB_ADMIN_TOKEN": "" 77 | } 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | #### 2. Restart Claude Desktop 84 | 85 | 86 | #### SSE mode 87 | 88 | Alternatively, you can run the MCP server in SSE mode by running the following command: 89 | 90 | ```bash 91 | uvx mcp-tinybird sse 92 | ``` 93 | 94 | This mode is useful to integrate with an MCP client that supports SSE (like a web app). 95 | 96 | ## Prompts 97 | 98 | The server provides a single prompt: 99 | - [tinybird-default](https://github.com/tinybirdco/mcp-tinybird/blob/93dd9e1d3c0e33f408fe88297151a44c1dfc049c/src/mcp-tinybird/server.py#L20): Assumes you have loaded some data in Tinybird and want help exploring it. 100 | - Requires a "topic" argument which defines the topic of the data you want to explore, for example, "Bluesky data" or "retail sales". 101 | 102 | You can configure additional prompt workflows: 103 | - Create a prompts Data Source in your workspace with this schema and append your prompts. The MCP loads `prompts` on initialization so you can configure it to your needs: 104 | ```bash 105 | SCHEMA > 106 | `name` String `json:$.name`, 107 | `description` String `json:$.description`, 108 | `timestamp` DateTime `json:$.timestamp`, 109 | `arguments` Array(String) `json:$.arguments[:]`, 110 | `prompt` String `json:$.prompt` 111 | ``` 112 | 113 | ## Tools 114 | 115 | The server implements several tools to interact with the Tinybird Workspace: 116 | - `list-data-sources`: Lists all Data Sources in the Tinybird Workspace 117 | - `list-pipes`: Lists all Pipe Endpoints in the Tinybird Workspace 118 | - `get-data-source`: Gets the information of a Data Source given its name, including the schema. 119 | - `get-pipe`: Gets the information of a Pipe Endpoint given its name, including its nodes and SQL transformation to understand what insights it provides. 120 | - `request-pipe-data`: Requests data from a Pipe Endpoints via an HTTP request. Pipe endpoints can have parameters to filter the analytical data. 121 | - `run-select-query`: Allows to run a select query over a Data Source to extract insights. 122 | - `append-insight`: Adds a new business insight to the memo resource 123 | - `llms-tinybird-docs`: Contains the whole Tinybird product documentation, so you can use it to get context about what Tinybird is, what it does, API reference and more. 124 | - `save-event`: This allows to send an event to a Tinybird Data Source. Use it to save a user generated prompt to the prompts Data Source. The MCP server feeds from the prompts Data Source on initialization so the user can instruct the LLM the workflow to follow. 125 | - `analyze-pipe`: Uses the Tinybird analyze API to run a ClickHouse explain on the Pipe Endpoint query and check if indexes, sorting key, and partition key are being used and propose optimizations suggestions 126 | - `push-datafile`: Creates a remote Data Source or Pipe in the Tinybird Workspace from a local datafile. Use the [Filesystem MCP](https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem) to save files generated by this MCP server. 127 | 128 | 129 | ## Development 130 | 131 | ### Config 132 | If you are working locally add two environment variables to a `.env` file in the root of the repository: 133 | 134 | ```sh 135 | TB_API_URL= 136 | TB_ADMIN_TOKEN= 137 | ``` 138 | 139 | For local development, update your Claude Desktop configuration: 140 | 141 | ```json 142 | { 143 | "mcpServers": { 144 | "mcp-tinybird_local": { 145 | "command": "uv", 146 | "args": [ 147 | "--directory", 148 | "/path/to/your/mcp-tinybird", 149 | "run", 150 | "mcp-tinybird", 151 | "stdio" 152 | ] 153 | } 154 | } 155 | } 156 | ``` 157 | 158 |
159 | Published Servers Configuration 160 | 161 | ```json 162 | "mcpServers": { 163 | "mcp-tinybird": { 164 | "command": "uvx", 165 | "args": [ 166 | "mcp-tinybird" 167 | ] 168 | } 169 | } 170 | ``` 171 |
172 | 173 | ### Building and Publishing 174 | 175 | To prepare the package for distribution: 176 | 177 | 1. Sync dependencies and update lockfile: 178 | ```bash 179 | uv sync 180 | ``` 181 | 182 | 2. Build package distributions: 183 | ```bash 184 | uv build 185 | ``` 186 | 187 | This will create source and wheel distributions in the `dist/` directory. 188 | 189 | 3. Publish to PyPI: 190 | ```bash 191 | uv publish 192 | ``` 193 | 194 | Note: You'll need to set PyPI credentials via environment variables or command flags: 195 | - Token: `--token` or `UV_PUBLISH_TOKEN` 196 | - Or username/password: `--username`/`UV_PUBLISH_USERNAME` and `--password`/`UV_PUBLISH_PASSWORD` 197 | 198 | ### Debugging 199 | 200 | Since MCP servers run over stdio, debugging can be challenging. For the best debugging 201 | experience, we strongly recommend using the [MCP Inspector](https://github.com/modelcontextprotocol/inspector). 202 | 203 | 204 | You can launch the MCP Inspector via [`npm`](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) with this command: 205 | 206 | ```bash 207 | npx @modelcontextprotocol/inspector uv --directory /Users/alrocar/gr/mcp-tinybird run mcp-tinybird 208 | ``` 209 | 210 | Upon launching, the Inspector will display a URL that you can access in your browser to begin debugging. 211 | 212 | ### Monitoring 213 | 214 | To monitor the MCP server, you can use any compatible Prometheus client such as [Grafana](https://grafana.com/). Learn how to monitor your MCP server [here](./mcp-analytics/README.md). 215 | 216 | -------------------------------------------------------------------------------- /bluesky-demo/.env.example: -------------------------------------------------------------------------------- 1 | TINYBIRD_SIGNING_KEY= 2 | TINYBIRD_WORKSPACE_ID= 3 | TINYBIRD_PIPES= -------------------------------------------------------------------------------- /bluesky-demo/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"] 3 | } 4 | -------------------------------------------------------------------------------- /bluesky-demo/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for committing if needed) 33 | .env* 34 | !.env.example 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /bluesky-demo/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /bluesky-demo/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } -------------------------------------------------------------------------------- /bluesky-demo/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /bluesky-demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluesky-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@radix-ui/react-slot": "^1.1.0", 13 | "class-variance-authority": "^0.7.1", 14 | "clsx": "^2.1.1", 15 | "jose": "^5.9.6", 16 | "lucide-react": "^0.464.0", 17 | "next": "^15.2.4", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0", 20 | "tailwind-merge": "^2.5.5", 21 | "tailwindcss-animate": "^1.0.7" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^20", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "eslint": "^8", 28 | "eslint-config-next": "15.0.3", 29 | "postcss": "^8", 30 | "tailwindcss": "^3.4.1", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /bluesky-demo/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /bluesky-demo/src/app/api/token/route.ts: -------------------------------------------------------------------------------- 1 | import { SignJWT } from 'jose' 2 | import { NextResponse } from 'next/server' 3 | 4 | export async function GET() { 5 | const signingKey = process.env.TINYBIRD_SIGNING_KEY 6 | const pipes = process.env.TINYBIRD_PIPES 7 | 8 | if (!signingKey) { 9 | return NextResponse.json( 10 | { error: 'TINYBIRD_SIGNING_KEY environment variable is not set' }, 11 | { status: 500 } 12 | ) 13 | } 14 | 15 | try { 16 | const encoder = new TextEncoder() 17 | const secretKey = encoder.encode(signingKey) 18 | 19 | // Generate scopes from pipes list 20 | const scopes = pipes 21 | ? pipes.split(',').map(pipe => ({ 22 | type: 'PIPES:READ', 23 | resource: pipe.trim() 24 | })) 25 | : [] 26 | 27 | const jwt = await new SignJWT({ 28 | workspace_id: process.env.TINYBIRD_WORKSPACE_ID, 29 | name: 'Tinybird Demo', 30 | scopes, 31 | limits: { rps: 1 } 32 | }) 33 | .setProtectedHeader({ alg: 'HS256' }) 34 | .setIssuedAt() 35 | .setExpirationTime('1d') 36 | .sign(secretKey) 37 | 38 | return NextResponse.json({ token: jwt }) 39 | } catch (error) { 40 | console.error('Error generating token:', error) 41 | return NextResponse.json( 42 | { error: 'Failed to generate token' }, 43 | { status: 500 } 44 | ) 45 | } 46 | } -------------------------------------------------------------------------------- /bluesky-demo/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/bluesky-demo/src/app/favicon.ico -------------------------------------------------------------------------------- /bluesky-demo/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/bluesky-demo/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /bluesky-demo/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/bluesky-demo/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /bluesky-demo/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body { 6 | font-family: Arial, Helvetica, sans-serif; 7 | } 8 | 9 | @layer base { 10 | :root { 11 | --background: 240 10% 3.9%; 12 | --foreground: 0 0% 98%; 13 | --card: 240 10% 3.9%; 14 | --card-foreground: 0 0% 98%; 15 | --popover: 240 10% 3.9%; 16 | --popover-foreground: 0 0% 98%; 17 | --primary: 0 0% 98%; 18 | --primary-foreground: 240 5.9% 10%; 19 | --secondary: 240 3.7% 15.9%; 20 | --secondary-foreground: 0 0% 98%; 21 | --muted: 240 3.7% 15.9%; 22 | --muted-foreground: 240 5% 64.9%; 23 | --accent: 240 3.7% 15.9%; 24 | --accent-foreground: 0 0% 98%; 25 | --destructive: 0 62.8% 30.6%; 26 | --destructive-foreground: 0 0% 98%; 27 | --border: 240 3.7% 15.9%; 28 | --input: 240 3.7% 15.9%; 29 | --ring: 240 4.9% 83.9%; 30 | --chart-1: 220 70% 50%; 31 | --chart-2: 160 60% 45%; 32 | --chart-3: 30 80% 55%; 33 | --chart-4: 280 65% 60%; 34 | --chart-5: 340 75% 55%; 35 | } 36 | } 37 | 38 | @layer base { 39 | * { 40 | @apply border-border; 41 | } 42 | body { 43 | @apply bg-background text-foreground; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /bluesky-demo/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import localFont from "next/font/local"; 3 | import "./globals.css"; 4 | 5 | const geistSans = localFont({ 6 | src: "./fonts/GeistVF.woff", 7 | variable: "--font-geist-sans", 8 | weight: "100 900", 9 | }); 10 | const geistMono = localFont({ 11 | src: "./fonts/GeistMonoVF.woff", 12 | variable: "--font-geist-mono", 13 | weight: "100 900", 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Bluesky Chat Demo", 18 | description: "Chat with Bluesky data", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /bluesky-demo/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { Input } from "@/components/ui/input" 5 | import { useState, useEffect } from "react" 6 | import { Copy, Check } from "lucide-react" 7 | 8 | export default function Home() { 9 | const [token, setToken] = useState("") 10 | const [copied, setCopied] = useState(false) 11 | const [error, setError] = useState("") 12 | 13 | useEffect(() => { 14 | const fetchToken = async () => { 15 | try { 16 | const response = await fetch('/api/token') 17 | const data = await response.json() 18 | 19 | if (data.error) { 20 | setError(data.error) 21 | } else { 22 | setToken(data.token) 23 | } 24 | } catch (err) { 25 | setError('Failed to fetch token: ' + err) 26 | } 27 | } 28 | 29 | fetchToken() 30 | }, []) 31 | 32 | const copyToClipboard = async () => { 33 | await navigator.clipboard.writeText(token) 34 | setCopied(true) 35 | setTimeout(() => setCopied(false), 2000) 36 | } 37 | 38 | return ( 39 |
40 |
41 |

Bluesky chat demo

42 |

This page is part of the mcp-tinybird Bluesky demo. Get your token below, and use with Claude Desktop to chat with Bluesky.

43 | 44 | {error ? ( 45 |

{error}

46 | ) : ( 47 |
48 | 53 | 65 |
66 | )} 67 |
68 |
69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /bluesky-demo/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /bluesky-demo/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ) 18 | } 19 | ) 20 | Input.displayName = "Input" 21 | 22 | export { Input } 23 | -------------------------------------------------------------------------------- /bluesky-demo/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /bluesky-demo/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["class"], 5 | content: [ 6 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | theme: { 11 | extend: { 12 | colors: { 13 | background: 'hsl(var(--background))', 14 | foreground: 'hsl(var(--foreground))', 15 | card: { 16 | DEFAULT: 'hsl(var(--card))', 17 | foreground: 'hsl(var(--card-foreground))' 18 | }, 19 | popover: { 20 | DEFAULT: 'hsl(var(--popover))', 21 | foreground: 'hsl(var(--popover-foreground))' 22 | }, 23 | primary: { 24 | DEFAULT: 'hsl(var(--primary))', 25 | foreground: 'hsl(var(--primary-foreground))' 26 | }, 27 | secondary: { 28 | DEFAULT: 'hsl(var(--secondary))', 29 | foreground: 'hsl(var(--secondary-foreground))' 30 | }, 31 | muted: { 32 | DEFAULT: 'hsl(var(--muted))', 33 | foreground: 'hsl(var(--muted-foreground))' 34 | }, 35 | accent: { 36 | DEFAULT: 'hsl(var(--accent))', 37 | foreground: 'hsl(var(--accent-foreground))' 38 | }, 39 | destructive: { 40 | DEFAULT: 'hsl(var(--destructive))', 41 | foreground: 'hsl(var(--destructive-foreground))' 42 | }, 43 | border: 'hsl(var(--border))', 44 | input: 'hsl(var(--input))', 45 | ring: 'hsl(var(--ring))', 46 | chart: { 47 | '1': 'hsl(var(--chart-1))', 48 | '2': 'hsl(var(--chart-2))', 49 | '3': 'hsl(var(--chart-3))', 50 | '4': 'hsl(var(--chart-4))', 51 | '5': 'hsl(var(--chart-5))' 52 | } 53 | }, 54 | borderRadius: { 55 | lg: 'var(--radius)', 56 | md: 'calc(var(--radius) - 2px)', 57 | sm: 'calc(var(--radius) - 4px)' 58 | } 59 | } 60 | }, 61 | plugins: [require("tailwindcss-animate")], 62 | } satisfies Config; 63 | -------------------------------------------------------------------------------- /bluesky-demo/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /dashboard-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/dashboard-template.png -------------------------------------------------------------------------------- /mcp-server-analytics/README.md: -------------------------------------------------------------------------------- 1 | # MCP analytics template for Tinybird 2 | 3 | This template includes the necessary code to implement remote MCP Server Analytics using Tinybird. 4 | 5 | [Watch a video demo](https://www.youtube.com/watch?v=8MlFALTsUqY) to learn how it works. 6 | 7 | ## Quick start 8 | 9 | Follow these steps to get started. 10 | 11 | ### Create a new Tinybird Workspace 12 | 13 | Click the button to deploy the project to your Tinybird Workspace. You'll be prompted to create a free Tinybird account if you don't yet have one. 14 | 15 | [![Deploy to Tinybird](https://cdn.tinybird.co/static/images/Tinybird-Deploy-Button.svg)](https://app.tinybird.co?starter_kit=https://github.com/tinybirdco/mcp-tinybird/mcp-server-analytics/tinybird) 16 | 17 | Deploying the project automatically creates: 18 | 19 | 1. Data Sources to store the log events 20 | 2. SQL Pipes to build metrics 21 | 3. Published API endpoints in Prometheus format 22 | 23 | ### Send log events 24 | 25 | #### Using Python 26 | 27 | Add the following dependency to your `requirements.txt` file: 28 | 29 | ``` 30 | tinybird-python-sdk>=0.1.6 31 | ``` 32 | 33 | Configure the logging handler: 34 | 35 | ```python 36 | import logging 37 | from multiprocessing import Queue 38 | from tb.logger import TinybirdLoggingQueueHandler 39 | from dotenv import load_dotenv 40 | 41 | load_dotenv() 42 | TB_API_URL = os.getenv("TB_API_URL") 43 | TB_WRITE_TOKEN = os.getenv("TB_WRITE_TOKEN") 44 | 45 | logger = logging.getLogger('your-logger-name') 46 | handler = TinybirdLoggingQueueHandler(Queue(-1), TB_API_URL, TB_WRITE_TOKEN, 'your-app-name', ds_name="mcp_logs_python") 47 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 48 | handler.setFormatter(formatter) 49 | logger.addHandler(handler) 50 | ``` 51 | 52 | - Replace `TB_WRITE_TOKEN` with the `mcp_public_write_token` located in the [Tinybird dashboard](https://app.tinybird.co/tokens). 53 | - Your `TB_API_URL` is the URL of your Tinybird region. See [Regions and endpoints](https://www.tinybird.co/docs/api-reference#regions-and-endpoints). 54 | 55 | To properly process your log events, add an extra dictionary with the `tool`, `resource`, `prompt`, `mcp_server_version` and `session` keys when it applies. That way the provided Tinybird Workspace will be able to process metrics by tool, resource, prompt and session. 56 | 57 | ```python 58 | logger.info(f"handle_call_tool {name}", extra={"session": session, "tool": name, "mcp_server_version": "0.1.4"}) 59 | ``` 60 | 61 | See some sample logger calls [here](https://github.com/tinybirdco/mcp-tinybird/blob/main/src/mcp_tinybird/server.py) 62 | 63 | #### Using TypeScript 64 | 65 | ```js 66 | const loggingToken = ""; 67 | const loggingEndpoint = "/v0/events?name=mcp_logs"; 68 | const loggingSession = crypto.randomUUID(); 69 | 70 | async function logger(level: string, record: object) { 71 | try { 72 | await fetch( 73 | loggingEndpoint, 74 | { 75 | method: 'POST', 76 | body: JSON.stringify({ 77 | timestamp: new Date().toISOString(), 78 | session: loggingSession, 79 | level: level, 80 | record: JSON.stringify(record) 81 | }), 82 | headers: { Authorization: `Bearer ${loggingToken}` } 83 | } 84 | ) 85 | .then((res: Response) => { /**process.stderr.write("logged");**/ }); 86 | } catch (error) { 87 | // process.stderr.write("error logging"); 88 | } 89 | ``` 90 | 91 | - Replace `TB_WRITE_TOKEN` with the `mcp_public_write_token` located in the [Tinybird dashboard](https://app.tinybird.co/tokens). 92 | - Your `TB_API_URL` is the URL of your Tinybird region. See [Regions and endpoints](https://www.tinybird.co/docs/api-reference#regions-and-endpoints). 93 | 94 | To properly process your log events, add the following keys to the `record` JSON object: 95 | 96 | ```js 97 | record = { 98 | "appName": "mcp-tinybird", 99 | "funcName": "handle_call_tool", 100 | "tool": "run-select-query", 101 | "prompt": "", 102 | "resource": "", 103 | "level": "info", 104 | "version": "0.1.4", 105 | "message": "this is a message" 106 | } 107 | ``` 108 | 109 | See some sample logger calls [here](See [ClaudeKeep](https://github.com/sdairs/claudekeep/blob/main/apps/mcp/src/index.ts) 110 | 111 | ### Monitor with Grafana and Prometheus 112 | 113 | Add this to your `prometheus.yml` file: 114 | 115 | ```yaml 116 | scrape_configs: 117 | - job_name: mcp_server 118 | scrape_interval: 15s # Adjust the scrape interval as needed 119 | scheme: 'https' 120 | static_configs: 121 | - targets: 122 | - 'api.tinybird.co' # Adjust this for your region if necessary 123 | metrics_path: '/v0/pipes/api_prometheus.prometheus' 124 | bearer_token: '' 125 | ``` 126 | 127 | Find `` in the [Tinybird dashboard](https://app.tinybird.co/tokens) with the name `prometheus`. 128 | 129 | You should start seeing your metrics in Grafana to build your own dashboards and alerts. 130 | 131 | ![](./prometheus.png) 132 | 133 | You can find a sample dashboard for Grafana can be found [here](./mcp-server-metrics-with-logs-v1.json). 134 | 135 | Click the image to watch a video on how to import the Dashboard into Grafana: 136 | 137 | [![Import Grafana Dashboard](./dashboard.png)](https://youtu.be/lOz5opvM24Q) 138 | 139 | ## Components 140 | 141 | The project uses Python and Typescript logging handlers to send events to the Tinybird [Events API](https://www.tinybird.co/docs/ingest/events-api) transforms the events and publishes metrics as Prometheus endpoints that you can integrate with your preferred observability tool. 142 | -------------------------------------------------------------------------------- /mcp-server-analytics/TEMPLATE.md: -------------------------------------------------------------------------------- 1 | The MCP Server Analytics template uses Python and Typescript logging handlers to send events to the Tinybird [Events API](https://www.tinybird.co/docs/ingest/events-api), which transforms the events and publishes metrics as Prometheus endpoints that you can integrate with your preferred observability tool. 2 | 3 | ## Set up the project 4 | 5 | Fork the GitHub repository and deploy the data project to Tinybird. 6 | 7 | ## Send log events 8 | 9 | ### Using Python 10 | 11 | Add the following dependency to your `requirements.txt` file: 12 | 13 | ``` 14 | tinybird-python-sdk>=0.1.6 15 | ``` 16 | 17 | Configure the logging handler: 18 | 19 | ```python 20 | import logging 21 | from multiprocessing import Queue 22 | from tb.logger import TinybirdLoggingQueueHandler 23 | from dotenv import load_dotenv 24 | 25 | load_dotenv() 26 | TB_API_URL = os.getenv("TB_API_URL") 27 | TB_WRITE_TOKEN = os.getenv("TB_WRITE_TOKEN") 28 | 29 | logger = logging.getLogger('your-logger-name') 30 | handler = TinybirdLoggingQueueHandler(Queue(-1), TB_API_URL, TB_WRITE_TOKEN, 'your-app-name', ds_name="mcp_logs_python") 31 | formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') 32 | handler.setFormatter(formatter) 33 | logger.addHandler(handler) 34 | ``` 35 | 36 | - Replace `TB_WRITE_TOKEN` with the `mcp_public_write_token` located in the [Tinybird dashboard](https://app.tinybird.co/tokens). 37 | - Your `TB_API_URL` is the URL of your Tinybird region. See [Regions and endpoints](https://www.tinybird.co/docs/api-reference#regions-and-endpoints). 38 | 39 | To properly process your log events, add an extra dictionary with the `tool`, `resource`, `prompt`, `mcp_server_version` and `session` keys when it applies. That way the provided Tinybird Workspace will be able to process metrics by tool, resource, prompt and session. 40 | 41 | ```python 42 | logger.info(f"handle_call_tool {name}", extra={"session": session, "tool": name, "mcp_server_version": "0.1.4"}) 43 | ``` 44 | 45 | #### Using TypeScript 46 | 47 | ```js 48 | const loggingToken = ""; 49 | const loggingEndpoint = "/v0/events?name=mcp_logs"; 50 | const loggingSession = crypto.randomUUID(); 51 | 52 | async function logger(level: string, record: object) { 53 | try { 54 | await fetch( 55 | loggingEndpoint, 56 | { 57 | method: 'POST', 58 | body: JSON.stringify({ 59 | timestamp: new Date().toISOString(), 60 | session: loggingSession, 61 | level: level, 62 | record: JSON.stringify(record) 63 | }), 64 | headers: { Authorization: `Bearer ${loggingToken}` } 65 | } 66 | ) 67 | .then((res: Response) => { /**process.stderr.write("logged");**/ }); 68 | } catch (error) { 69 | // process.stderr.write("error logging"); 70 | } 71 | ``` 72 | 73 | - Replace `TB_WRITE_TOKEN` with the `mcp_public_write_token` located in the [Tinybird dashboard](https://app.tinybird.co/tokens). 74 | - Your `TB_API_URL` is the URL of your Tinybird region. See [Regions and endpoints](https://www.tinybird.co/docs/api-reference#regions-and-endpoints). 75 | 76 | To properly process your log events, add the following keys to the `record` JSON object: 77 | 78 | ```js 79 | record = { 80 | "appName": "mcp-tinybird", 81 | "funcName": "handle_call_tool", 82 | "tool": "run-select-query", 83 | "prompt": "", 84 | "resource": "", 85 | "level": "info", 86 | "version": "0.1.4", 87 | "message": "this is a message" 88 | } 89 | ``` 90 | 91 | ### Monitor with Grafana and Prometheus 92 | 93 | Add this to your `prometheus.yml` file: 94 | 95 | ```yaml 96 | scrape_configs: 97 | - job_name: mcp_server 98 | scrape_interval: 15s # Adjust the scrape interval as needed 99 | scheme: 'https' 100 | static_configs: 101 | - targets: 102 | - 'api.tinybird.co' # Adjust this for your region if necessary 103 | metrics_path: '/v0/pipes/api_prometheus.prometheus' 104 | bearer_token: '' 105 | ``` 106 | 107 | Find `` in the [Tinybird dashboard](https://app.tinybird.co/tokens) with the name `prometheus`. 108 | 109 | You should start seeing your metrics in Grafana to build your own dashboards and alerts. 110 | -------------------------------------------------------------------------------- /mcp-server-analytics/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/mcp-server-analytics/dashboard.png -------------------------------------------------------------------------------- /mcp-server-analytics/mcp-server-metrics-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__elements": {}, 13 | "__requires": [ 14 | { 15 | "type": "panel", 16 | "id": "bargauge", 17 | "name": "Bar gauge", 18 | "version": "" 19 | }, 20 | { 21 | "type": "grafana", 22 | "id": "grafana", 23 | "name": "Grafana", 24 | "version": "11.3.1" 25 | }, 26 | { 27 | "type": "datasource", 28 | "id": "prometheus", 29 | "name": "Prometheus", 30 | "version": "1.0.0" 31 | }, 32 | { 33 | "type": "panel", 34 | "id": "stat", 35 | "name": "Stat", 36 | "version": "" 37 | }, 38 | { 39 | "type": "panel", 40 | "id": "timeseries", 41 | "name": "Time series", 42 | "version": "" 43 | } 44 | ], 45 | "annotations": { 46 | "list": [ 47 | { 48 | "builtIn": 1, 49 | "datasource": { 50 | "type": "grafana", 51 | "uid": "-- Grafana --" 52 | }, 53 | "enable": true, 54 | "hide": true, 55 | "iconColor": "rgba(0, 211, 255, 1)", 56 | "name": "Annotations & Alerts", 57 | "type": "dashboard" 58 | } 59 | ] 60 | }, 61 | "editable": true, 62 | "fiscalYearStartMonth": 0, 63 | "graphTooltip": 0, 64 | "id": null, 65 | "links": [], 66 | "panels": [ 67 | { 68 | "datasource": { 69 | "type": "prometheus", 70 | "uid": "${DS_PROMETHEUS}" 71 | }, 72 | "fieldConfig": { 73 | "defaults": { 74 | "color": { 75 | "mode": "thresholds" 76 | }, 77 | "mappings": [], 78 | "thresholds": { 79 | "mode": "absolute", 80 | "steps": [ 81 | { 82 | "color": "green", 83 | "value": null 84 | }, 85 | { 86 | "color": "red", 87 | "value": 80 88 | } 89 | ] 90 | } 91 | }, 92 | "overrides": [] 93 | }, 94 | "gridPos": { 95 | "h": 3, 96 | "w": 5, 97 | "x": 0, 98 | "y": 0 99 | }, 100 | "id": 8, 101 | "options": { 102 | "colorMode": "value", 103 | "graphMode": "area", 104 | "justifyMode": "auto", 105 | "orientation": "horizontal", 106 | "percentChangeColorMode": "standard", 107 | "reduceOptions": { 108 | "calcs": [ 109 | "lastNotNull" 110 | ], 111 | "fields": "", 112 | "values": false 113 | }, 114 | "showPercentChange": false, 115 | "textMode": "auto", 116 | "wideLayout": true 117 | }, 118 | "pluginVersion": "11.3.1", 119 | "targets": [ 120 | { 121 | "disableTextWrap": false, 122 | "editorMode": "builder", 123 | "expr": "mcp_uniq_total_sessions{app_name=~\"$app_name\", version=~\"$version\"}", 124 | "fullMetaSearch": false, 125 | "includeNullMetadata": true, 126 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 127 | "range": true, 128 | "refId": "A", 129 | "useBackend": false, 130 | "datasource": { 131 | "type": "prometheus", 132 | "uid": "${DS_PROMETHEUS}" 133 | } 134 | } 135 | ], 136 | "title": "Uniq Total Sessions", 137 | "type": "stat" 138 | }, 139 | { 140 | "datasource": { 141 | "type": "prometheus", 142 | "uid": "${DS_PROMETHEUS}" 143 | }, 144 | "fieldConfig": { 145 | "defaults": { 146 | "color": { 147 | "mode": "palette-classic" 148 | }, 149 | "custom": { 150 | "axisBorderShow": false, 151 | "axisCenteredZero": false, 152 | "axisColorMode": "text", 153 | "axisLabel": "", 154 | "axisPlacement": "auto", 155 | "barAlignment": 0, 156 | "barWidthFactor": 0.6, 157 | "drawStyle": "line", 158 | "fillOpacity": 0, 159 | "gradientMode": "none", 160 | "hideFrom": { 161 | "legend": false, 162 | "tooltip": false, 163 | "viz": false 164 | }, 165 | "insertNulls": false, 166 | "lineInterpolation": "linear", 167 | "lineWidth": 1, 168 | "pointSize": 5, 169 | "scaleDistribution": { 170 | "type": "linear" 171 | }, 172 | "showPoints": "auto", 173 | "spanNulls": false, 174 | "stacking": { 175 | "group": "A", 176 | "mode": "none" 177 | }, 178 | "thresholdsStyle": { 179 | "mode": "off" 180 | } 181 | }, 182 | "mappings": [], 183 | "thresholds": { 184 | "mode": "absolute", 185 | "steps": [ 186 | { 187 | "color": "green", 188 | "value": null 189 | }, 190 | { 191 | "color": "red", 192 | "value": 80 193 | } 194 | ] 195 | } 196 | }, 197 | "overrides": [] 198 | }, 199 | "gridPos": { 200 | "h": 3, 201 | "w": 6, 202 | "x": 5, 203 | "y": 0 204 | }, 205 | "id": 3, 206 | "options": { 207 | "legend": { 208 | "calcs": [], 209 | "displayMode": "list", 210 | "placement": "right", 211 | "showLegend": true 212 | }, 213 | "tooltip": { 214 | "mode": "single", 215 | "sort": "none" 216 | } 217 | }, 218 | "pluginVersion": "11.3.1", 219 | "targets": [ 220 | { 221 | "disableTextWrap": false, 222 | "editorMode": "builder", 223 | "expr": "mcp_uniq_total_sessions{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 224 | "fullMetaSearch": false, 225 | "includeNullMetadata": true, 226 | "legendFormat": "{{app_name}}[{{version}}]", 227 | "range": true, 228 | "refId": "A", 229 | "useBackend": false, 230 | "datasource": { 231 | "type": "prometheus", 232 | "uid": "${DS_PROMETHEUS}" 233 | } 234 | } 235 | ], 236 | "title": "Uniq sessions", 237 | "type": "timeseries" 238 | }, 239 | { 240 | "datasource": { 241 | "type": "prometheus", 242 | "uid": "${DS_PROMETHEUS}" 243 | }, 244 | "fieldConfig": { 245 | "defaults": { 246 | "color": { 247 | "mode": "palette-classic" 248 | }, 249 | "custom": { 250 | "axisBorderShow": false, 251 | "axisCenteredZero": false, 252 | "axisColorMode": "text", 253 | "axisLabel": "", 254 | "axisPlacement": "auto", 255 | "barAlignment": 0, 256 | "barWidthFactor": 0.6, 257 | "drawStyle": "line", 258 | "fillOpacity": 0, 259 | "gradientMode": "none", 260 | "hideFrom": { 261 | "legend": false, 262 | "tooltip": false, 263 | "viz": false 264 | }, 265 | "insertNulls": false, 266 | "lineInterpolation": "linear", 267 | "lineWidth": 1, 268 | "pointSize": 5, 269 | "scaleDistribution": { 270 | "type": "linear" 271 | }, 272 | "showPoints": "auto", 273 | "spanNulls": false, 274 | "stacking": { 275 | "group": "A", 276 | "mode": "none" 277 | }, 278 | "thresholdsStyle": { 279 | "mode": "off" 280 | } 281 | }, 282 | "mappings": [], 283 | "thresholds": { 284 | "mode": "absolute", 285 | "steps": [ 286 | { 287 | "color": "green", 288 | "value": null 289 | }, 290 | { 291 | "color": "red", 292 | "value": 80 293 | } 294 | ] 295 | } 296 | }, 297 | "overrides": [] 298 | }, 299 | "gridPos": { 300 | "h": 3, 301 | "w": 7, 302 | "x": 11, 303 | "y": 0 304 | }, 305 | "id": 9, 306 | "options": { 307 | "legend": { 308 | "calcs": [], 309 | "displayMode": "list", 310 | "placement": "right", 311 | "showLegend": true 312 | }, 313 | "tooltip": { 314 | "mode": "single", 315 | "sort": "none" 316 | } 317 | }, 318 | "pluginVersion": "11.3.1", 319 | "targets": [ 320 | { 321 | "disableTextWrap": false, 322 | "editorMode": "builder", 323 | "expr": "sum by(app_name) (mcp_requests_total{app_name=~\"$app_name\"})", 324 | "fullMetaSearch": false, 325 | "includeNullMetadata": true, 326 | "legendFormat": "__auto", 327 | "range": true, 328 | "refId": "A", 329 | "useBackend": false, 330 | "datasource": { 331 | "type": "prometheus", 332 | "uid": "${DS_PROMETHEUS}" 333 | } 334 | } 335 | ], 336 | "title": "Total requests", 337 | "type": "timeseries" 338 | }, 339 | { 340 | "datasource": { 341 | "type": "prometheus", 342 | "uid": "${DS_PROMETHEUS}" 343 | }, 344 | "fieldConfig": { 345 | "defaults": { 346 | "color": { 347 | "mode": "palette-classic" 348 | }, 349 | "custom": { 350 | "axisBorderShow": false, 351 | "axisCenteredZero": false, 352 | "axisColorMode": "text", 353 | "axisLabel": "", 354 | "axisPlacement": "auto", 355 | "barAlignment": 0, 356 | "barWidthFactor": 0.6, 357 | "drawStyle": "line", 358 | "fillOpacity": 0, 359 | "gradientMode": "none", 360 | "hideFrom": { 361 | "legend": false, 362 | "tooltip": false, 363 | "viz": false 364 | }, 365 | "insertNulls": false, 366 | "lineInterpolation": "linear", 367 | "lineWidth": 1, 368 | "pointSize": 5, 369 | "scaleDistribution": { 370 | "type": "linear" 371 | }, 372 | "showPoints": "auto", 373 | "spanNulls": false, 374 | "stacking": { 375 | "group": "A", 376 | "mode": "none" 377 | }, 378 | "thresholdsStyle": { 379 | "mode": "off" 380 | } 381 | }, 382 | "mappings": [], 383 | "thresholds": { 384 | "mode": "absolute", 385 | "steps": [ 386 | { 387 | "color": "green", 388 | "value": null 389 | }, 390 | { 391 | "color": "red", 392 | "value": 80 393 | } 394 | ] 395 | } 396 | }, 397 | "overrides": [] 398 | }, 399 | "gridPos": { 400 | "h": 3, 401 | "w": 6, 402 | "x": 18, 403 | "y": 0 404 | }, 405 | "id": 10, 406 | "options": { 407 | "legend": { 408 | "calcs": [], 409 | "displayMode": "list", 410 | "placement": "right", 411 | "showLegend": true 412 | }, 413 | "tooltip": { 414 | "mode": "single", 415 | "sort": "none" 416 | } 417 | }, 418 | "pluginVersion": "11.3.1", 419 | "targets": [ 420 | { 421 | "disableTextWrap": false, 422 | "editorMode": "builder", 423 | "expr": "sum by(app_name) (mcp_errors_total{app_name=~\"$app_name\"})", 424 | "fullMetaSearch": false, 425 | "includeNullMetadata": true, 426 | "legendFormat": "__auto", 427 | "range": true, 428 | "refId": "A", 429 | "useBackend": false, 430 | "datasource": { 431 | "type": "prometheus", 432 | "uid": "${DS_PROMETHEUS}" 433 | } 434 | } 435 | ], 436 | "title": "Total errors", 437 | "type": "timeseries" 438 | }, 439 | { 440 | "datasource": { 441 | "type": "prometheus", 442 | "uid": "${DS_PROMETHEUS}" 443 | }, 444 | "fieldConfig": { 445 | "defaults": { 446 | "color": { 447 | "mode": "thresholds" 448 | }, 449 | "mappings": [], 450 | "thresholds": { 451 | "mode": "absolute", 452 | "steps": [ 453 | { 454 | "color": "green", 455 | "value": null 456 | }, 457 | { 458 | "color": "red", 459 | "value": 80 460 | } 461 | ] 462 | } 463 | }, 464 | "overrides": [] 465 | }, 466 | "gridPos": { 467 | "h": 5, 468 | "w": 10, 469 | "x": 0, 470 | "y": 3 471 | }, 472 | "id": 6, 473 | "options": { 474 | "displayMode": "gradient", 475 | "legend": { 476 | "calcs": [], 477 | "displayMode": "list", 478 | "placement": "bottom", 479 | "showLegend": false 480 | }, 481 | "maxVizHeight": 300, 482 | "minVizHeight": 16, 483 | "minVizWidth": 8, 484 | "namePlacement": "auto", 485 | "orientation": "horizontal", 486 | "reduceOptions": { 487 | "calcs": [ 488 | "lastNotNull" 489 | ], 490 | "fields": "", 491 | "values": false 492 | }, 493 | "showUnfilled": true, 494 | "sizing": "auto", 495 | "valueMode": "color" 496 | }, 497 | "pluginVersion": "11.3.1", 498 | "targets": [ 499 | { 500 | "disableTextWrap": false, 501 | "editorMode": "builder", 502 | "expr": "mcp_requests_total{app_name=~\"$app_name\", tool=~\"$tool\"}", 503 | "fullMetaSearch": false, 504 | "includeNullMetadata": true, 505 | "legendFormat": "{{tool}}", 506 | "range": true, 507 | "refId": "A", 508 | "useBackend": false, 509 | "datasource": { 510 | "type": "prometheus", 511 | "uid": "${DS_PROMETHEUS}" 512 | } 513 | } 514 | ], 515 | "title": "Requests by tool", 516 | "type": "bargauge" 517 | }, 518 | { 519 | "datasource": { 520 | "type": "prometheus", 521 | "uid": "${DS_PROMETHEUS}" 522 | }, 523 | "fieldConfig": { 524 | "defaults": { 525 | "color": { 526 | "mode": "palette-classic" 527 | }, 528 | "custom": { 529 | "axisBorderShow": false, 530 | "axisCenteredZero": false, 531 | "axisColorMode": "text", 532 | "axisLabel": "", 533 | "axisPlacement": "auto", 534 | "barAlignment": 0, 535 | "barWidthFactor": 0.6, 536 | "drawStyle": "line", 537 | "fillOpacity": 0, 538 | "gradientMode": "none", 539 | "hideFrom": { 540 | "legend": false, 541 | "tooltip": false, 542 | "viz": false 543 | }, 544 | "insertNulls": false, 545 | "lineInterpolation": "linear", 546 | "lineWidth": 1, 547 | "pointSize": 5, 548 | "scaleDistribution": { 549 | "type": "linear" 550 | }, 551 | "showPoints": "auto", 552 | "spanNulls": false, 553 | "stacking": { 554 | "group": "A", 555 | "mode": "none" 556 | }, 557 | "thresholdsStyle": { 558 | "mode": "off" 559 | } 560 | }, 561 | "mappings": [], 562 | "thresholds": { 563 | "mode": "absolute", 564 | "steps": [ 565 | { 566 | "color": "green", 567 | "value": null 568 | }, 569 | { 570 | "color": "red", 571 | "value": 80 572 | } 573 | ] 574 | } 575 | }, 576 | "overrides": [] 577 | }, 578 | "gridPos": { 579 | "h": 5, 580 | "w": 14, 581 | "x": 10, 582 | "y": 3 583 | }, 584 | "id": 1, 585 | "options": { 586 | "legend": { 587 | "calcs": [], 588 | "displayMode": "list", 589 | "placement": "right", 590 | "showLegend": true 591 | }, 592 | "tooltip": { 593 | "mode": "single", 594 | "sort": "none" 595 | } 596 | }, 597 | "pluginVersion": "11.3.1", 598 | "targets": [ 599 | { 600 | "datasource": { 601 | "type": "prometheus", 602 | "uid": "${DS_PROMETHEUS}" 603 | }, 604 | "disableTextWrap": false, 605 | "editorMode": "builder", 606 | "expr": "mcp_requests_total{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 607 | "fullMetaSearch": false, 608 | "includeNullMetadata": true, 609 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 610 | "range": true, 611 | "refId": "A", 612 | "useBackend": false 613 | } 614 | ], 615 | "title": "Total requests", 616 | "type": "timeseries" 617 | }, 618 | { 619 | "datasource": { 620 | "type": "prometheus", 621 | "uid": "${DS_PROMETHEUS}" 622 | }, 623 | "fieldConfig": { 624 | "defaults": { 625 | "color": { 626 | "mode": "thresholds" 627 | }, 628 | "mappings": [], 629 | "thresholds": { 630 | "mode": "absolute", 631 | "steps": [ 632 | { 633 | "color": "green", 634 | "value": null 635 | }, 636 | { 637 | "color": "red", 638 | "value": 80 639 | } 640 | ] 641 | } 642 | }, 643 | "overrides": [] 644 | }, 645 | "gridPos": { 646 | "h": 5, 647 | "w": 10, 648 | "x": 0, 649 | "y": 8 650 | }, 651 | "id": 7, 652 | "options": { 653 | "displayMode": "gradient", 654 | "legend": { 655 | "calcs": [], 656 | "displayMode": "list", 657 | "placement": "bottom", 658 | "showLegend": false 659 | }, 660 | "maxVizHeight": 300, 661 | "minVizHeight": 16, 662 | "minVizWidth": 8, 663 | "namePlacement": "auto", 664 | "orientation": "horizontal", 665 | "reduceOptions": { 666 | "calcs": [ 667 | "lastNotNull" 668 | ], 669 | "fields": "", 670 | "values": false 671 | }, 672 | "showUnfilled": true, 673 | "sizing": "auto", 674 | "valueMode": "color" 675 | }, 676 | "pluginVersion": "11.3.1", 677 | "targets": [ 678 | { 679 | "disableTextWrap": false, 680 | "editorMode": "builder", 681 | "expr": "mcp_errors_total{app_name=~\"$app_name\", tool=~\"$tool\"}", 682 | "fullMetaSearch": false, 683 | "includeNullMetadata": true, 684 | "legendFormat": "{{tool}}", 685 | "range": true, 686 | "refId": "A", 687 | "useBackend": false, 688 | "datasource": { 689 | "type": "prometheus", 690 | "uid": "${DS_PROMETHEUS}" 691 | } 692 | } 693 | ], 694 | "title": "Errors by tool", 695 | "type": "bargauge" 696 | }, 697 | { 698 | "datasource": { 699 | "type": "prometheus", 700 | "uid": "${DS_PROMETHEUS}" 701 | }, 702 | "fieldConfig": { 703 | "defaults": { 704 | "color": { 705 | "mode": "palette-classic" 706 | }, 707 | "custom": { 708 | "axisBorderShow": false, 709 | "axisCenteredZero": false, 710 | "axisColorMode": "text", 711 | "axisLabel": "", 712 | "axisPlacement": "auto", 713 | "barAlignment": 0, 714 | "barWidthFactor": 0.6, 715 | "drawStyle": "line", 716 | "fillOpacity": 0, 717 | "gradientMode": "none", 718 | "hideFrom": { 719 | "legend": false, 720 | "tooltip": false, 721 | "viz": false 722 | }, 723 | "insertNulls": false, 724 | "lineInterpolation": "linear", 725 | "lineWidth": 1, 726 | "pointSize": 5, 727 | "scaleDistribution": { 728 | "type": "linear" 729 | }, 730 | "showPoints": "auto", 731 | "spanNulls": false, 732 | "stacking": { 733 | "group": "A", 734 | "mode": "none" 735 | }, 736 | "thresholdsStyle": { 737 | "mode": "off" 738 | } 739 | }, 740 | "mappings": [], 741 | "thresholds": { 742 | "mode": "absolute", 743 | "steps": [ 744 | { 745 | "color": "green", 746 | "value": null 747 | }, 748 | { 749 | "color": "red", 750 | "value": 80 751 | } 752 | ] 753 | } 754 | }, 755 | "overrides": [] 756 | }, 757 | "gridPos": { 758 | "h": 5, 759 | "w": 14, 760 | "x": 10, 761 | "y": 8 762 | }, 763 | "id": 2, 764 | "options": { 765 | "legend": { 766 | "calcs": [], 767 | "displayMode": "list", 768 | "placement": "right", 769 | "showLegend": true 770 | }, 771 | "tooltip": { 772 | "mode": "single", 773 | "sort": "none" 774 | } 775 | }, 776 | "pluginVersion": "11.3.1", 777 | "targets": [ 778 | { 779 | "disableTextWrap": false, 780 | "editorMode": "builder", 781 | "expr": "mcp_errors_total{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 782 | "format": "time_series", 783 | "fullMetaSearch": false, 784 | "includeNullMetadata": true, 785 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 786 | "range": true, 787 | "refId": "A", 788 | "useBackend": false, 789 | "datasource": { 790 | "type": "prometheus", 791 | "uid": "${DS_PROMETHEUS}" 792 | } 793 | } 794 | ], 795 | "title": "Total errors", 796 | "type": "timeseries" 797 | }, 798 | { 799 | "datasource": { 800 | "type": "prometheus", 801 | "uid": "${DS_PROMETHEUS}" 802 | }, 803 | "fieldConfig": { 804 | "defaults": { 805 | "color": { 806 | "mode": "palette-classic" 807 | }, 808 | "custom": { 809 | "axisBorderShow": false, 810 | "axisCenteredZero": false, 811 | "axisColorMode": "text", 812 | "axisLabel": "", 813 | "axisPlacement": "auto", 814 | "barAlignment": 0, 815 | "barWidthFactor": 0.6, 816 | "drawStyle": "line", 817 | "fillOpacity": 0, 818 | "gradientMode": "none", 819 | "hideFrom": { 820 | "legend": false, 821 | "tooltip": false, 822 | "viz": false 823 | }, 824 | "insertNulls": false, 825 | "lineInterpolation": "linear", 826 | "lineWidth": 1, 827 | "pointSize": 5, 828 | "scaleDistribution": { 829 | "type": "linear" 830 | }, 831 | "showPoints": "auto", 832 | "spanNulls": false, 833 | "stacking": { 834 | "group": "A", 835 | "mode": "none" 836 | }, 837 | "thresholdsStyle": { 838 | "mode": "off" 839 | } 840 | }, 841 | "mappings": [], 842 | "thresholds": { 843 | "mode": "absolute", 844 | "steps": [ 845 | { 846 | "color": "green", 847 | "value": null 848 | }, 849 | { 850 | "color": "red", 851 | "value": 80 852 | } 853 | ] 854 | } 855 | }, 856 | "overrides": [] 857 | }, 858 | "gridPos": { 859 | "h": 8, 860 | "w": 12, 861 | "x": 0, 862 | "y": 13 863 | }, 864 | "id": 4, 865 | "options": { 866 | "legend": { 867 | "calcs": [], 868 | "displayMode": "list", 869 | "placement": "right", 870 | "showLegend": true 871 | }, 872 | "tooltip": { 873 | "mode": "single", 874 | "sort": "none" 875 | } 876 | }, 877 | "pluginVersion": "11.3.1", 878 | "targets": [ 879 | { 880 | "disableTextWrap": false, 881 | "editorMode": "builder", 882 | "exemplar": false, 883 | "expr": "mcp_requests_total_5m{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 884 | "fullMetaSearch": false, 885 | "includeNullMetadata": true, 886 | "instant": false, 887 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 888 | "range": true, 889 | "refId": "A", 890 | "useBackend": false, 891 | "datasource": { 892 | "type": "prometheus", 893 | "uid": "${DS_PROMETHEUS}" 894 | } 895 | } 896 | ], 897 | "title": "Rate of Requests", 898 | "type": "timeseries" 899 | }, 900 | { 901 | "datasource": { 902 | "type": "prometheus", 903 | "uid": "${DS_PROMETHEUS}" 904 | }, 905 | "fieldConfig": { 906 | "defaults": { 907 | "color": { 908 | "mode": "palette-classic" 909 | }, 910 | "custom": { 911 | "axisBorderShow": false, 912 | "axisCenteredZero": false, 913 | "axisColorMode": "text", 914 | "axisLabel": "", 915 | "axisPlacement": "auto", 916 | "barAlignment": 0, 917 | "barWidthFactor": 0.6, 918 | "drawStyle": "line", 919 | "fillOpacity": 0, 920 | "gradientMode": "none", 921 | "hideFrom": { 922 | "legend": false, 923 | "tooltip": false, 924 | "viz": false 925 | }, 926 | "insertNulls": false, 927 | "lineInterpolation": "linear", 928 | "lineWidth": 1, 929 | "pointSize": 5, 930 | "scaleDistribution": { 931 | "type": "linear" 932 | }, 933 | "showPoints": "auto", 934 | "spanNulls": false, 935 | "stacking": { 936 | "group": "A", 937 | "mode": "none" 938 | }, 939 | "thresholdsStyle": { 940 | "mode": "off" 941 | } 942 | }, 943 | "mappings": [], 944 | "thresholds": { 945 | "mode": "absolute", 946 | "steps": [ 947 | { 948 | "color": "green", 949 | "value": null 950 | }, 951 | { 952 | "color": "red", 953 | "value": 80 954 | } 955 | ] 956 | } 957 | }, 958 | "overrides": [] 959 | }, 960 | "gridPos": { 961 | "h": 8, 962 | "w": 12, 963 | "x": 12, 964 | "y": 13 965 | }, 966 | "id": 5, 967 | "options": { 968 | "legend": { 969 | "calcs": [], 970 | "displayMode": "list", 971 | "placement": "right", 972 | "showLegend": true 973 | }, 974 | "tooltip": { 975 | "mode": "single", 976 | "sort": "none" 977 | } 978 | }, 979 | "pluginVersion": "11.3.1", 980 | "targets": [ 981 | { 982 | "disableTextWrap": false, 983 | "editorMode": "builder", 984 | "expr": "mcp_errors_total_5m{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 985 | "fullMetaSearch": false, 986 | "includeNullMetadata": true, 987 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 988 | "range": true, 989 | "refId": "A", 990 | "useBackend": false, 991 | "datasource": { 992 | "type": "prometheus", 993 | "uid": "${DS_PROMETHEUS}" 994 | } 995 | } 996 | ], 997 | "title": "Rate of Errors", 998 | "type": "timeseries" 999 | } 1000 | ], 1001 | "schemaVersion": 40, 1002 | "tags": [], 1003 | "templating": { 1004 | "list": [ 1005 | { 1006 | "current": {}, 1007 | "definition": "label_values(app_name)", 1008 | "includeAll": false, 1009 | "multi": true, 1010 | "name": "app_name", 1011 | "options": [], 1012 | "query": { 1013 | "qryType": 1, 1014 | "query": "label_values(app_name)", 1015 | "refId": "PrometheusVariableQueryEditor-VariableQuery" 1016 | }, 1017 | "refresh": 1, 1018 | "regex": "", 1019 | "type": "query" 1020 | }, 1021 | { 1022 | "current": {}, 1023 | "definition": "label_values(version)", 1024 | "includeAll": true, 1025 | "multi": true, 1026 | "name": "version", 1027 | "options": [], 1028 | "query": { 1029 | "qryType": 1, 1030 | "query": "label_values(version)", 1031 | "refId": "PrometheusVariableQueryEditor-VariableQuery" 1032 | }, 1033 | "refresh": 1, 1034 | "regex": "", 1035 | "type": "query" 1036 | }, 1037 | { 1038 | "current": {}, 1039 | "definition": "label_values(tool)", 1040 | "includeAll": true, 1041 | "multi": true, 1042 | "name": "tool", 1043 | "options": [], 1044 | "query": { 1045 | "qryType": 1, 1046 | "query": "label_values(tool)", 1047 | "refId": "PrometheusVariableQueryEditor-VariableQuery" 1048 | }, 1049 | "refresh": 1, 1050 | "regex": "", 1051 | "type": "query" 1052 | } 1053 | ] 1054 | }, 1055 | "time": { 1056 | "from": "now-6h", 1057 | "to": "now" 1058 | }, 1059 | "timepicker": {}, 1060 | "timezone": "browser", 1061 | "title": "MCP metrics", 1062 | "uid": "ee6e0s657e2o0c", 1063 | "version": 5, 1064 | "weekStart": "" 1065 | } -------------------------------------------------------------------------------- /mcp-server-analytics/mcp-server-metrics-with-logs-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | }, 11 | { 12 | "name": "DS_MCP-METRICS-DATASOURCE", 13 | "label": "mcp-metrics-datasource", 14 | "description": "", 15 | "type": "datasource", 16 | "pluginId": "yesoreyeram-infinity-datasource", 17 | "pluginName": "Infinity" 18 | } 19 | ], 20 | "__elements": {}, 21 | "__requires": [ 22 | { 23 | "type": "panel", 24 | "id": "bargauge", 25 | "name": "Bar gauge", 26 | "version": "" 27 | }, 28 | { 29 | "type": "grafana", 30 | "id": "grafana", 31 | "name": "Grafana", 32 | "version": "11.3.1" 33 | }, 34 | { 35 | "type": "datasource", 36 | "id": "prometheus", 37 | "name": "Prometheus", 38 | "version": "1.0.0" 39 | }, 40 | { 41 | "type": "panel", 42 | "id": "stat", 43 | "name": "Stat", 44 | "version": "" 45 | }, 46 | { 47 | "type": "panel", 48 | "id": "table", 49 | "name": "Table", 50 | "version": "" 51 | }, 52 | { 53 | "type": "panel", 54 | "id": "timeseries", 55 | "name": "Time series", 56 | "version": "" 57 | }, 58 | { 59 | "type": "datasource", 60 | "id": "yesoreyeram-infinity-datasource", 61 | "name": "Infinity", 62 | "version": "2.11.4" 63 | } 64 | ], 65 | "annotations": { 66 | "list": [ 67 | { 68 | "builtIn": 1, 69 | "datasource": { 70 | "type": "grafana", 71 | "uid": "-- Grafana --" 72 | }, 73 | "enable": true, 74 | "hide": true, 75 | "iconColor": "rgba(0, 211, 255, 1)", 76 | "name": "Annotations & Alerts", 77 | "type": "dashboard" 78 | } 79 | ] 80 | }, 81 | "editable": true, 82 | "fiscalYearStartMonth": 0, 83 | "graphTooltip": 0, 84 | "id": null, 85 | "links": [], 86 | "panels": [ 87 | { 88 | "datasource": { 89 | "type": "prometheus", 90 | "uid": "${DS_PROMETHEUS}" 91 | }, 92 | "fieldConfig": { 93 | "defaults": { 94 | "color": { 95 | "mode": "thresholds" 96 | }, 97 | "mappings": [], 98 | "thresholds": { 99 | "mode": "absolute", 100 | "steps": [ 101 | { 102 | "color": "green", 103 | "value": null 104 | }, 105 | { 106 | "color": "red", 107 | "value": 80 108 | } 109 | ] 110 | } 111 | }, 112 | "overrides": [] 113 | }, 114 | "gridPos": { 115 | "h": 3, 116 | "w": 5, 117 | "x": 0, 118 | "y": 0 119 | }, 120 | "id": 8, 121 | "options": { 122 | "colorMode": "value", 123 | "graphMode": "area", 124 | "justifyMode": "auto", 125 | "orientation": "horizontal", 126 | "percentChangeColorMode": "standard", 127 | "reduceOptions": { 128 | "calcs": [ 129 | "lastNotNull" 130 | ], 131 | "fields": "", 132 | "values": false 133 | }, 134 | "showPercentChange": false, 135 | "textMode": "auto", 136 | "wideLayout": true 137 | }, 138 | "pluginVersion": "11.3.1", 139 | "targets": [ 140 | { 141 | "disableTextWrap": false, 142 | "editorMode": "builder", 143 | "expr": "mcp_uniq_total_sessions{app_name=~\"$app_name\", version=~\"$version\"}", 144 | "fullMetaSearch": false, 145 | "includeNullMetadata": true, 146 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 147 | "range": true, 148 | "refId": "A", 149 | "useBackend": false, 150 | "datasource": { 151 | "type": "prometheus", 152 | "uid": "${DS_PROMETHEUS}" 153 | } 154 | } 155 | ], 156 | "title": "Uniq Total Sessions", 157 | "type": "stat" 158 | }, 159 | { 160 | "datasource": { 161 | "type": "prometheus", 162 | "uid": "${DS_PROMETHEUS}" 163 | }, 164 | "fieldConfig": { 165 | "defaults": { 166 | "color": { 167 | "mode": "palette-classic" 168 | }, 169 | "custom": { 170 | "axisBorderShow": false, 171 | "axisCenteredZero": false, 172 | "axisColorMode": "text", 173 | "axisLabel": "", 174 | "axisPlacement": "auto", 175 | "barAlignment": 0, 176 | "barWidthFactor": 0.6, 177 | "drawStyle": "line", 178 | "fillOpacity": 0, 179 | "gradientMode": "none", 180 | "hideFrom": { 181 | "legend": false, 182 | "tooltip": false, 183 | "viz": false 184 | }, 185 | "insertNulls": false, 186 | "lineInterpolation": "linear", 187 | "lineWidth": 1, 188 | "pointSize": 5, 189 | "scaleDistribution": { 190 | "type": "linear" 191 | }, 192 | "showPoints": "auto", 193 | "spanNulls": false, 194 | "stacking": { 195 | "group": "A", 196 | "mode": "none" 197 | }, 198 | "thresholdsStyle": { 199 | "mode": "off" 200 | } 201 | }, 202 | "mappings": [], 203 | "thresholds": { 204 | "mode": "absolute", 205 | "steps": [ 206 | { 207 | "color": "green", 208 | "value": null 209 | }, 210 | { 211 | "color": "red", 212 | "value": 80 213 | } 214 | ] 215 | } 216 | }, 217 | "overrides": [] 218 | }, 219 | "gridPos": { 220 | "h": 3, 221 | "w": 6, 222 | "x": 5, 223 | "y": 0 224 | }, 225 | "id": 3, 226 | "options": { 227 | "legend": { 228 | "calcs": [], 229 | "displayMode": "list", 230 | "placement": "right", 231 | "showLegend": true 232 | }, 233 | "tooltip": { 234 | "mode": "single", 235 | "sort": "none" 236 | } 237 | }, 238 | "pluginVersion": "11.3.1", 239 | "targets": [ 240 | { 241 | "disableTextWrap": false, 242 | "editorMode": "builder", 243 | "expr": "mcp_uniq_total_sessions{app_name=~\"$app_name\", version=~\"$version\"}", 244 | "fullMetaSearch": false, 245 | "includeNullMetadata": true, 246 | "legendFormat": "{{app_name}}[{{version}}]", 247 | "range": true, 248 | "refId": "A", 249 | "useBackend": false, 250 | "datasource": { 251 | "type": "prometheus", 252 | "uid": "${DS_PROMETHEUS}" 253 | } 254 | } 255 | ], 256 | "title": "Uniq sessions", 257 | "type": "timeseries" 258 | }, 259 | { 260 | "datasource": { 261 | "type": "prometheus", 262 | "uid": "${DS_PROMETHEUS}" 263 | }, 264 | "fieldConfig": { 265 | "defaults": { 266 | "color": { 267 | "mode": "palette-classic" 268 | }, 269 | "custom": { 270 | "axisBorderShow": false, 271 | "axisCenteredZero": false, 272 | "axisColorMode": "text", 273 | "axisLabel": "", 274 | "axisPlacement": "auto", 275 | "barAlignment": 0, 276 | "barWidthFactor": 0.6, 277 | "drawStyle": "line", 278 | "fillOpacity": 0, 279 | "gradientMode": "none", 280 | "hideFrom": { 281 | "legend": false, 282 | "tooltip": false, 283 | "viz": false 284 | }, 285 | "insertNulls": false, 286 | "lineInterpolation": "linear", 287 | "lineWidth": 1, 288 | "pointSize": 5, 289 | "scaleDistribution": { 290 | "type": "linear" 291 | }, 292 | "showPoints": "auto", 293 | "spanNulls": false, 294 | "stacking": { 295 | "group": "A", 296 | "mode": "none" 297 | }, 298 | "thresholdsStyle": { 299 | "mode": "off" 300 | } 301 | }, 302 | "mappings": [], 303 | "thresholds": { 304 | "mode": "absolute", 305 | "steps": [ 306 | { 307 | "color": "green", 308 | "value": null 309 | }, 310 | { 311 | "color": "red", 312 | "value": 80 313 | } 314 | ] 315 | } 316 | }, 317 | "overrides": [] 318 | }, 319 | "gridPos": { 320 | "h": 3, 321 | "w": 7, 322 | "x": 11, 323 | "y": 0 324 | }, 325 | "id": 9, 326 | "options": { 327 | "legend": { 328 | "calcs": [], 329 | "displayMode": "list", 330 | "placement": "right", 331 | "showLegend": true 332 | }, 333 | "tooltip": { 334 | "mode": "single", 335 | "sort": "none" 336 | } 337 | }, 338 | "pluginVersion": "11.3.1", 339 | "targets": [ 340 | { 341 | "disableTextWrap": false, 342 | "editorMode": "builder", 343 | "expr": "sum by(app_name) (mcp_requests_total{app_name=~\"$app_name\"})", 344 | "fullMetaSearch": false, 345 | "includeNullMetadata": true, 346 | "legendFormat": "__auto", 347 | "range": true, 348 | "refId": "A", 349 | "useBackend": false, 350 | "datasource": { 351 | "type": "prometheus", 352 | "uid": "${DS_PROMETHEUS}" 353 | } 354 | } 355 | ], 356 | "title": "Total requests", 357 | "type": "timeseries" 358 | }, 359 | { 360 | "datasource": { 361 | "type": "prometheus", 362 | "uid": "${DS_PROMETHEUS}" 363 | }, 364 | "fieldConfig": { 365 | "defaults": { 366 | "color": { 367 | "mode": "palette-classic" 368 | }, 369 | "custom": { 370 | "axisBorderShow": false, 371 | "axisCenteredZero": false, 372 | "axisColorMode": "text", 373 | "axisLabel": "", 374 | "axisPlacement": "auto", 375 | "barAlignment": 0, 376 | "barWidthFactor": 0.6, 377 | "drawStyle": "line", 378 | "fillOpacity": 0, 379 | "gradientMode": "none", 380 | "hideFrom": { 381 | "legend": false, 382 | "tooltip": false, 383 | "viz": false 384 | }, 385 | "insertNulls": false, 386 | "lineInterpolation": "linear", 387 | "lineWidth": 1, 388 | "pointSize": 5, 389 | "scaleDistribution": { 390 | "type": "linear" 391 | }, 392 | "showPoints": "auto", 393 | "spanNulls": false, 394 | "stacking": { 395 | "group": "A", 396 | "mode": "none" 397 | }, 398 | "thresholdsStyle": { 399 | "mode": "off" 400 | } 401 | }, 402 | "mappings": [], 403 | "thresholds": { 404 | "mode": "absolute", 405 | "steps": [ 406 | { 407 | "color": "green", 408 | "value": null 409 | }, 410 | { 411 | "color": "red", 412 | "value": 80 413 | } 414 | ] 415 | } 416 | }, 417 | "overrides": [] 418 | }, 419 | "gridPos": { 420 | "h": 3, 421 | "w": 6, 422 | "x": 18, 423 | "y": 0 424 | }, 425 | "id": 10, 426 | "options": { 427 | "legend": { 428 | "calcs": [], 429 | "displayMode": "list", 430 | "placement": "right", 431 | "showLegend": true 432 | }, 433 | "tooltip": { 434 | "mode": "single", 435 | "sort": "none" 436 | } 437 | }, 438 | "pluginVersion": "11.3.1", 439 | "targets": [ 440 | { 441 | "disableTextWrap": false, 442 | "editorMode": "builder", 443 | "expr": "sum by(app_name) (mcp_errors_total{app_name=~\"$app_name\"})", 444 | "fullMetaSearch": false, 445 | "includeNullMetadata": true, 446 | "legendFormat": "__auto", 447 | "range": true, 448 | "refId": "A", 449 | "useBackend": false, 450 | "datasource": { 451 | "type": "prometheus", 452 | "uid": "${DS_PROMETHEUS}" 453 | } 454 | } 455 | ], 456 | "title": "Total errors", 457 | "type": "timeseries" 458 | }, 459 | { 460 | "datasource": { 461 | "type": "prometheus", 462 | "uid": "${DS_PROMETHEUS}" 463 | }, 464 | "fieldConfig": { 465 | "defaults": { 466 | "color": { 467 | "mode": "thresholds" 468 | }, 469 | "mappings": [], 470 | "thresholds": { 471 | "mode": "absolute", 472 | "steps": [ 473 | { 474 | "color": "green", 475 | "value": null 476 | }, 477 | { 478 | "color": "red", 479 | "value": 80 480 | } 481 | ] 482 | } 483 | }, 484 | "overrides": [] 485 | }, 486 | "gridPos": { 487 | "h": 5, 488 | "w": 10, 489 | "x": 0, 490 | "y": 3 491 | }, 492 | "id": 6, 493 | "options": { 494 | "displayMode": "gradient", 495 | "legend": { 496 | "calcs": [], 497 | "displayMode": "list", 498 | "placement": "bottom", 499 | "showLegend": false 500 | }, 501 | "maxVizHeight": 300, 502 | "minVizHeight": 16, 503 | "minVizWidth": 8, 504 | "namePlacement": "auto", 505 | "orientation": "horizontal", 506 | "reduceOptions": { 507 | "calcs": [ 508 | "lastNotNull" 509 | ], 510 | "fields": "", 511 | "values": false 512 | }, 513 | "showUnfilled": true, 514 | "sizing": "auto", 515 | "valueMode": "color" 516 | }, 517 | "pluginVersion": "11.3.1", 518 | "targets": [ 519 | { 520 | "disableTextWrap": false, 521 | "editorMode": "builder", 522 | "expr": "mcp_requests_total{app_name=~\"$app_name\", tool=~\"$tool\"}", 523 | "fullMetaSearch": false, 524 | "includeNullMetadata": true, 525 | "legendFormat": "{{tool}}", 526 | "range": true, 527 | "refId": "A", 528 | "useBackend": false, 529 | "datasource": { 530 | "type": "prometheus", 531 | "uid": "${DS_PROMETHEUS}" 532 | } 533 | } 534 | ], 535 | "title": "Requests by tool", 536 | "type": "bargauge" 537 | }, 538 | { 539 | "datasource": { 540 | "type": "prometheus", 541 | "uid": "${DS_PROMETHEUS}" 542 | }, 543 | "fieldConfig": { 544 | "defaults": { 545 | "color": { 546 | "mode": "palette-classic" 547 | }, 548 | "custom": { 549 | "axisBorderShow": false, 550 | "axisCenteredZero": false, 551 | "axisColorMode": "text", 552 | "axisLabel": "", 553 | "axisPlacement": "auto", 554 | "barAlignment": 0, 555 | "barWidthFactor": 0.6, 556 | "drawStyle": "line", 557 | "fillOpacity": 0, 558 | "gradientMode": "none", 559 | "hideFrom": { 560 | "legend": false, 561 | "tooltip": false, 562 | "viz": false 563 | }, 564 | "insertNulls": false, 565 | "lineInterpolation": "linear", 566 | "lineWidth": 1, 567 | "pointSize": 5, 568 | "scaleDistribution": { 569 | "type": "linear" 570 | }, 571 | "showPoints": "auto", 572 | "spanNulls": false, 573 | "stacking": { 574 | "group": "A", 575 | "mode": "none" 576 | }, 577 | "thresholdsStyle": { 578 | "mode": "off" 579 | } 580 | }, 581 | "mappings": [], 582 | "thresholds": { 583 | "mode": "absolute", 584 | "steps": [ 585 | { 586 | "color": "green", 587 | "value": null 588 | }, 589 | { 590 | "color": "red", 591 | "value": 80 592 | } 593 | ] 594 | } 595 | }, 596 | "overrides": [] 597 | }, 598 | "gridPos": { 599 | "h": 5, 600 | "w": 14, 601 | "x": 10, 602 | "y": 3 603 | }, 604 | "id": 1, 605 | "options": { 606 | "legend": { 607 | "calcs": [], 608 | "displayMode": "list", 609 | "placement": "right", 610 | "showLegend": true 611 | }, 612 | "tooltip": { 613 | "mode": "single", 614 | "sort": "none" 615 | } 616 | }, 617 | "pluginVersion": "11.3.1", 618 | "targets": [ 619 | { 620 | "datasource": { 621 | "type": "prometheus", 622 | "uid": "${DS_PROMETHEUS}" 623 | }, 624 | "disableTextWrap": false, 625 | "editorMode": "builder", 626 | "expr": "mcp_requests_total{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 627 | "fullMetaSearch": false, 628 | "includeNullMetadata": true, 629 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 630 | "range": true, 631 | "refId": "A", 632 | "useBackend": false 633 | } 634 | ], 635 | "title": "Total requests", 636 | "type": "timeseries" 637 | }, 638 | { 639 | "datasource": { 640 | "type": "prometheus", 641 | "uid": "${DS_PROMETHEUS}" 642 | }, 643 | "fieldConfig": { 644 | "defaults": { 645 | "color": { 646 | "mode": "thresholds" 647 | }, 648 | "mappings": [], 649 | "thresholds": { 650 | "mode": "absolute", 651 | "steps": [ 652 | { 653 | "color": "green", 654 | "value": null 655 | }, 656 | { 657 | "color": "red", 658 | "value": 80 659 | } 660 | ] 661 | } 662 | }, 663 | "overrides": [] 664 | }, 665 | "gridPos": { 666 | "h": 5, 667 | "w": 10, 668 | "x": 0, 669 | "y": 8 670 | }, 671 | "id": 7, 672 | "options": { 673 | "displayMode": "gradient", 674 | "legend": { 675 | "calcs": [], 676 | "displayMode": "list", 677 | "placement": "bottom", 678 | "showLegend": false 679 | }, 680 | "maxVizHeight": 300, 681 | "minVizHeight": 16, 682 | "minVizWidth": 8, 683 | "namePlacement": "auto", 684 | "orientation": "horizontal", 685 | "reduceOptions": { 686 | "calcs": [ 687 | "lastNotNull" 688 | ], 689 | "fields": "", 690 | "values": false 691 | }, 692 | "showUnfilled": true, 693 | "sizing": "auto", 694 | "valueMode": "color" 695 | }, 696 | "pluginVersion": "11.3.1", 697 | "targets": [ 698 | { 699 | "disableTextWrap": false, 700 | "editorMode": "builder", 701 | "expr": "mcp_errors_total{app_name=~\"$app_name\", tool=~\"$tool\"}", 702 | "fullMetaSearch": false, 703 | "includeNullMetadata": true, 704 | "legendFormat": "{{tool}}", 705 | "range": true, 706 | "refId": "A", 707 | "useBackend": false, 708 | "datasource": { 709 | "type": "prometheus", 710 | "uid": "${DS_PROMETHEUS}" 711 | } 712 | } 713 | ], 714 | "title": "Errors by tool", 715 | "type": "bargauge" 716 | }, 717 | { 718 | "datasource": { 719 | "type": "prometheus", 720 | "uid": "${DS_PROMETHEUS}" 721 | }, 722 | "fieldConfig": { 723 | "defaults": { 724 | "color": { 725 | "mode": "palette-classic" 726 | }, 727 | "custom": { 728 | "axisBorderShow": false, 729 | "axisCenteredZero": false, 730 | "axisColorMode": "text", 731 | "axisLabel": "", 732 | "axisPlacement": "auto", 733 | "barAlignment": 0, 734 | "barWidthFactor": 0.6, 735 | "drawStyle": "line", 736 | "fillOpacity": 0, 737 | "gradientMode": "none", 738 | "hideFrom": { 739 | "legend": false, 740 | "tooltip": false, 741 | "viz": false 742 | }, 743 | "insertNulls": false, 744 | "lineInterpolation": "linear", 745 | "lineWidth": 1, 746 | "pointSize": 5, 747 | "scaleDistribution": { 748 | "type": "linear" 749 | }, 750 | "showPoints": "auto", 751 | "spanNulls": false, 752 | "stacking": { 753 | "group": "A", 754 | "mode": "none" 755 | }, 756 | "thresholdsStyle": { 757 | "mode": "off" 758 | } 759 | }, 760 | "mappings": [], 761 | "thresholds": { 762 | "mode": "absolute", 763 | "steps": [ 764 | { 765 | "color": "green", 766 | "value": null 767 | }, 768 | { 769 | "color": "red", 770 | "value": 80 771 | } 772 | ] 773 | } 774 | }, 775 | "overrides": [] 776 | }, 777 | "gridPos": { 778 | "h": 5, 779 | "w": 14, 780 | "x": 10, 781 | "y": 8 782 | }, 783 | "id": 2, 784 | "options": { 785 | "legend": { 786 | "calcs": [], 787 | "displayMode": "list", 788 | "placement": "right", 789 | "showLegend": true 790 | }, 791 | "tooltip": { 792 | "mode": "single", 793 | "sort": "none" 794 | } 795 | }, 796 | "pluginVersion": "11.3.1", 797 | "targets": [ 798 | { 799 | "disableTextWrap": false, 800 | "editorMode": "builder", 801 | "expr": "mcp_errors_total{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 802 | "format": "time_series", 803 | "fullMetaSearch": false, 804 | "includeNullMetadata": true, 805 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 806 | "range": true, 807 | "refId": "A", 808 | "useBackend": false, 809 | "datasource": { 810 | "type": "prometheus", 811 | "uid": "${DS_PROMETHEUS}" 812 | } 813 | } 814 | ], 815 | "title": "Total errors", 816 | "type": "timeseries" 817 | }, 818 | { 819 | "datasource": { 820 | "type": "prometheus", 821 | "uid": "${DS_PROMETHEUS}" 822 | }, 823 | "fieldConfig": { 824 | "defaults": { 825 | "color": { 826 | "mode": "palette-classic" 827 | }, 828 | "custom": { 829 | "axisBorderShow": false, 830 | "axisCenteredZero": false, 831 | "axisColorMode": "text", 832 | "axisLabel": "", 833 | "axisPlacement": "auto", 834 | "barAlignment": 0, 835 | "barWidthFactor": 0.6, 836 | "drawStyle": "line", 837 | "fillOpacity": 0, 838 | "gradientMode": "none", 839 | "hideFrom": { 840 | "legend": false, 841 | "tooltip": false, 842 | "viz": false 843 | }, 844 | "insertNulls": false, 845 | "lineInterpolation": "linear", 846 | "lineWidth": 1, 847 | "pointSize": 5, 848 | "scaleDistribution": { 849 | "type": "linear" 850 | }, 851 | "showPoints": "auto", 852 | "spanNulls": false, 853 | "stacking": { 854 | "group": "A", 855 | "mode": "none" 856 | }, 857 | "thresholdsStyle": { 858 | "mode": "off" 859 | } 860 | }, 861 | "mappings": [], 862 | "thresholds": { 863 | "mode": "absolute", 864 | "steps": [ 865 | { 866 | "color": "green", 867 | "value": null 868 | }, 869 | { 870 | "color": "red", 871 | "value": 80 872 | } 873 | ] 874 | } 875 | }, 876 | "overrides": [] 877 | }, 878 | "gridPos": { 879 | "h": 8, 880 | "w": 12, 881 | "x": 0, 882 | "y": 13 883 | }, 884 | "id": 4, 885 | "options": { 886 | "legend": { 887 | "calcs": [], 888 | "displayMode": "list", 889 | "placement": "right", 890 | "showLegend": true 891 | }, 892 | "tooltip": { 893 | "mode": "single", 894 | "sort": "none" 895 | } 896 | }, 897 | "pluginVersion": "11.3.1", 898 | "targets": [ 899 | { 900 | "disableTextWrap": false, 901 | "editorMode": "builder", 902 | "exemplar": false, 903 | "expr": "mcp_requests_total_5m{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 904 | "fullMetaSearch": false, 905 | "includeNullMetadata": true, 906 | "instant": false, 907 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 908 | "range": true, 909 | "refId": "A", 910 | "useBackend": false, 911 | "datasource": { 912 | "type": "prometheus", 913 | "uid": "${DS_PROMETHEUS}" 914 | } 915 | } 916 | ], 917 | "title": "Rate of Requests", 918 | "type": "timeseries" 919 | }, 920 | { 921 | "datasource": { 922 | "type": "prometheus", 923 | "uid": "${DS_PROMETHEUS}" 924 | }, 925 | "fieldConfig": { 926 | "defaults": { 927 | "color": { 928 | "mode": "palette-classic" 929 | }, 930 | "custom": { 931 | "axisBorderShow": false, 932 | "axisCenteredZero": false, 933 | "axisColorMode": "text", 934 | "axisLabel": "", 935 | "axisPlacement": "auto", 936 | "barAlignment": 0, 937 | "barWidthFactor": 0.6, 938 | "drawStyle": "line", 939 | "fillOpacity": 0, 940 | "gradientMode": "none", 941 | "hideFrom": { 942 | "legend": false, 943 | "tooltip": false, 944 | "viz": false 945 | }, 946 | "insertNulls": false, 947 | "lineInterpolation": "linear", 948 | "lineWidth": 1, 949 | "pointSize": 5, 950 | "scaleDistribution": { 951 | "type": "linear" 952 | }, 953 | "showPoints": "auto", 954 | "spanNulls": false, 955 | "stacking": { 956 | "group": "A", 957 | "mode": "none" 958 | }, 959 | "thresholdsStyle": { 960 | "mode": "off" 961 | } 962 | }, 963 | "mappings": [], 964 | "thresholds": { 965 | "mode": "absolute", 966 | "steps": [ 967 | { 968 | "color": "green", 969 | "value": null 970 | }, 971 | { 972 | "color": "red", 973 | "value": 80 974 | } 975 | ] 976 | } 977 | }, 978 | "overrides": [] 979 | }, 980 | "gridPos": { 981 | "h": 8, 982 | "w": 12, 983 | "x": 12, 984 | "y": 13 985 | }, 986 | "id": 5, 987 | "options": { 988 | "legend": { 989 | "calcs": [], 990 | "displayMode": "list", 991 | "placement": "right", 992 | "showLegend": true 993 | }, 994 | "tooltip": { 995 | "mode": "single", 996 | "sort": "none" 997 | } 998 | }, 999 | "pluginVersion": "11.3.1", 1000 | "targets": [ 1001 | { 1002 | "disableTextWrap": false, 1003 | "editorMode": "builder", 1004 | "expr": "mcp_errors_total_5m{app_name=~\"$app_name\", version=~\"$version\", tool=~\"$tool\"}", 1005 | "fullMetaSearch": false, 1006 | "includeNullMetadata": true, 1007 | "legendFormat": "{{app_name}}[{{version}}] - {{tool}}", 1008 | "range": true, 1009 | "refId": "A", 1010 | "useBackend": false, 1011 | "datasource": { 1012 | "type": "prometheus", 1013 | "uid": "${DS_PROMETHEUS}" 1014 | } 1015 | } 1016 | ], 1017 | "title": "Rate of Errors", 1018 | "type": "timeseries" 1019 | }, 1020 | { 1021 | "datasource": { 1022 | "type": "yesoreyeram-infinity-datasource", 1023 | "uid": "${DS_MCP-METRICS-DATASOURCE}" 1024 | }, 1025 | "fieldConfig": { 1026 | "defaults": { 1027 | "color": { 1028 | "mode": "thresholds" 1029 | }, 1030 | "custom": { 1031 | "align": "auto", 1032 | "cellOptions": { 1033 | "type": "auto" 1034 | }, 1035 | "filterable": true, 1036 | "inspect": false 1037 | }, 1038 | "mappings": [], 1039 | "thresholds": { 1040 | "mode": "absolute", 1041 | "steps": [ 1042 | { 1043 | "color": "green", 1044 | "value": null 1045 | }, 1046 | { 1047 | "color": "red", 1048 | "value": 80 1049 | } 1050 | ] 1051 | } 1052 | }, 1053 | "overrides": [] 1054 | }, 1055 | "gridPos": { 1056 | "h": 22, 1057 | "w": 24, 1058 | "x": 0, 1059 | "y": 21 1060 | }, 1061 | "id": 11, 1062 | "options": { 1063 | "cellHeight": "sm", 1064 | "footer": { 1065 | "countRows": false, 1066 | "enablePagination": true, 1067 | "fields": "", 1068 | "reducer": [ 1069 | "sum" 1070 | ], 1071 | "show": false 1072 | }, 1073 | "showHeader": true, 1074 | "sortBy": [ 1075 | { 1076 | "desc": false, 1077 | "displayName": "datetime" 1078 | } 1079 | ] 1080 | }, 1081 | "pluginVersion": "11.3.1", 1082 | "targets": [ 1083 | { 1084 | "columns": [], 1085 | "datasource": { 1086 | "type": "yesoreyeram-infinity-datasource", 1087 | "uid": "${DS_MCP-METRICS-DATASOURCE}" 1088 | }, 1089 | "filters": [], 1090 | "format": "table", 1091 | "global_query_id": "", 1092 | "parser": "backend", 1093 | "refId": "A", 1094 | "root_selector": "data", 1095 | "source": "url", 1096 | "type": "json", 1097 | "url": "https://api.tinybird.co/v0/pipes/api_logs.json", 1098 | "url_options": { 1099 | "data": "", 1100 | "method": "GET", 1101 | "params": [ 1102 | { 1103 | "key": "date_from", 1104 | "value": "$__from" 1105 | }, 1106 | { 1107 | "key": "date_to", 1108 | "value": "$__to" 1109 | }, 1110 | { 1111 | "key": "app_name", 1112 | "value": "$app_name" 1113 | }, 1114 | { 1115 | "key": "version", 1116 | "value": "$version" 1117 | }, 1118 | { 1119 | "key": "log", 1120 | "value": "$log" 1121 | } 1122 | ] 1123 | } 1124 | } 1125 | ], 1126 | "title": "Logs", 1127 | "type": "table" 1128 | } 1129 | ], 1130 | "schemaVersion": 40, 1131 | "tags": [], 1132 | "templating": { 1133 | "list": [ 1134 | { 1135 | "current": {}, 1136 | "definition": "label_values(app_name)", 1137 | "includeAll": false, 1138 | "multi": true, 1139 | "name": "app_name", 1140 | "options": [], 1141 | "query": { 1142 | "qryType": 1, 1143 | "query": "label_values(app_name)", 1144 | "refId": "PrometheusVariableQueryEditor-VariableQuery" 1145 | }, 1146 | "refresh": 1, 1147 | "regex": "", 1148 | "type": "query" 1149 | }, 1150 | { 1151 | "current": {}, 1152 | "definition": "label_values(version)", 1153 | "includeAll": true, 1154 | "multi": true, 1155 | "name": "version", 1156 | "options": [], 1157 | "query": { 1158 | "qryType": 1, 1159 | "query": "label_values(version)", 1160 | "refId": "PrometheusVariableQueryEditor-VariableQuery" 1161 | }, 1162 | "refresh": 1, 1163 | "regex": "", 1164 | "type": "query" 1165 | }, 1166 | { 1167 | "current": {}, 1168 | "definition": "label_values(tool)", 1169 | "includeAll": true, 1170 | "multi": true, 1171 | "name": "tool", 1172 | "options": [], 1173 | "query": { 1174 | "qryType": 1, 1175 | "query": "label_values(tool)", 1176 | "refId": "PrometheusVariableQueryEditor-VariableQuery" 1177 | }, 1178 | "refresh": 1, 1179 | "regex": "", 1180 | "type": "query" 1181 | }, 1182 | { 1183 | "current": { 1184 | "text": "", 1185 | "value": "" 1186 | }, 1187 | "description": "filter message logs by the text introduced", 1188 | "name": "log", 1189 | "options": [ 1190 | { 1191 | "selected": true, 1192 | "text": "", 1193 | "value": "" 1194 | } 1195 | ], 1196 | "query": "", 1197 | "type": "textbox" 1198 | } 1199 | ] 1200 | }, 1201 | "time": { 1202 | "from": "now-15m", 1203 | "to": "now" 1204 | }, 1205 | "timepicker": {}, 1206 | "timezone": "browser", 1207 | "title": "MCP metrics", 1208 | "uid": "ee6e0s657e2o0c", 1209 | "version": 10, 1210 | "weekStart": "" 1211 | } -------------------------------------------------------------------------------- /mcp-server-analytics/prometheus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/mcp-server-analytics/prometheus.png -------------------------------------------------------------------------------- /mcp-server-analytics/region.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/mcp-server-analytics/region.png -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/.tinyenv: -------------------------------------------------------------------------------- 1 | 2 | VERSION=0.0.0 3 | 4 | 5 | 6 | ########## 7 | # OPTIONAL env vars 8 | 9 | # Don't print CLI version warning message if there's a new available version 10 | # TB_VERSION_WARNING=0 11 | 12 | # Skip regression tests 13 | TB_SKIP_REGRESSION=1 14 | 15 | # Use `OBFUSCATE_REGEX_PATTERN` and `OBFUSCATE_PATTERN_SEPARATOR` environment variables to define a regex pattern and a separator (in case of a single string with multiple regex) to obfuscate secrets in the CLI output. 16 | # OBFUSCATE_REGEX_PATTERN="https://(www\.)?[^/]+||^Follow these instructions =>" 17 | # OBFUSCATE_PATTERN_SEPARATOR=|| 18 | ########## 19 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/fixtures/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/mcp-server-analytics/tinybird/datasources/fixtures/.gitkeep -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/mcp_logs.datasource: -------------------------------------------------------------------------------- 1 | TOKEN "mcp_public_write_token" APPEND 2 | 3 | 4 | SCHEMA > 5 | `timestamp` DateTime `json:$.timestamp`, 6 | `session` String `json:$.session`, 7 | `level` String `json:$.level`, 8 | `record` String `json:$.record` 9 | 10 | ENGINE "MergeTree" 11 | ENGINE_PARTITION_KEY "toYear(timestamp)" 12 | ENGINE_SORTING_KEY "session, timestamp" 13 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/mcp_logs_python.datasource: -------------------------------------------------------------------------------- 1 | TOKEN "mcp_public_write_token" APPEND 2 | 3 | 4 | SCHEMA > 5 | `app_name` String `json:$.app_name`, 6 | `asctime` DateTime64(3) `json:$.asctime`, 7 | `created` Float64 `json:$.created`, 8 | `extra_levelno` Int16 `json:$.extra.levelno`, 9 | `extra_msg` String `json:$.extra.msg`, 10 | `filename` String `json:$.filename`, 11 | `formatted_message` String `json:$.formatted_message`, 12 | `funcName` String `json:$.funcName`, 13 | `level` Int16 `json:$.level`, 14 | `levelname` String `json:$.levelname`, 15 | `lineno` Int16 `json:$.lineno`, 16 | `message` String `json:$.message`, 17 | `module` String `json:$.module`, 18 | `msecs` Float32 `json:$.msecs`, 19 | `name` String `json:$.name`, 20 | `pathname` String `json:$.pathname`, 21 | `process` Int32 `json:$.process`, 22 | `processName` String `json:$.processName`, 23 | `relativeCreated` Float64 `json:$.relativeCreated`, 24 | `thread` Int64 `json:$.thread`, 25 | `threadName` String `json:$.threadName`, 26 | `extra_session` String `json:$.extra.session`, 27 | `extra_mcp_server_version` Nullable(String) `json:$.extra.mcp_server_version`, 28 | `extra_tool` Nullable(String) `json:$.extra.tool`, 29 | `extra_prompt` Nullable(String) `json:$.extra.prompt`, 30 | `extra_resource` Nullable(String) `json:$.extra.resource` 31 | 32 | ENGINE "MergeTree" 33 | ENGINE_PARTITION_KEY "toYYYYMM(asctime)" 34 | ENGINE_SORTING_KEY "app_name, asctime" 35 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/mcp_monitoring.datasource: -------------------------------------------------------------------------------- 1 | # Data Source created from Pipe 'mv_mcp_logs_python_pipe' 2 | 3 | SCHEMA > 4 | `app_name` String `json:$.app_name`, 5 | `datetime` DateTime64(3) `json:$.datetime`, 6 | `func_name` LowCardinality(String) `json:$.func_name`, 7 | `tool` LowCardinality(String) `json:$.tool`, 8 | `prompt` LowCardinality(String) `json:$.prompt`, 9 | `resource` LowCardinality(String) `json:$.resource`, 10 | `level` LowCardinality(String) `json:$.level`, 11 | `session` String `json:$.session`, 12 | `version` Nullable(String) `json:$.version`, 13 | `message` String `json:$.message` 14 | 15 | ENGINE "MergeTree" 16 | ENGINE_PARTITION_KEY "toYYYYMM(datetime)" 17 | ENGINE_SORTING_KEY "app_name, datetime" 18 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/mv_count_mcp_errors_by_tool.datasource: -------------------------------------------------------------------------------- 1 | # Data Source created from Pipe 'count_mcp_errors_by_app' 2 | 3 | SCHEMA > 4 | `datetime` DateTime, 5 | `app_name` String, 6 | `version` String, 7 | `tool` String, 8 | `errors` AggregateFunction(count) 9 | 10 | ENGINE "AggregatingMergeTree" 11 | ENGINE_PARTITION_KEY "toYYYYMM(datetime)" 12 | ENGINE_SORTING_KEY "datetime, app_name, version, tool" 13 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/mv_count_mcp_logs_by_prompt.datasource: -------------------------------------------------------------------------------- 1 | # Data Source created from Pipe 'count_mcp_logs_by_app' 2 | 3 | SCHEMA > 4 | `datetime` DateTime, 5 | `app_name` String, 6 | `version` String, 7 | `prompt` String, 8 | `requests` AggregateFunction(count) 9 | 10 | ENGINE "AggregatingMergeTree" 11 | ENGINE_PARTITION_KEY "toYYYYMM(datetime)" 12 | ENGINE_SORTING_KEY "datetime, app_name, version, prompt" 13 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/mv_count_mcp_logs_by_tool.datasource: -------------------------------------------------------------------------------- 1 | # Data Source created from Pipe 'count_mcp_logs_by_app' 2 | 3 | SCHEMA > 4 | `datetime` DateTime, 5 | `app_name` String, 6 | `version` String, 7 | `tool` String, 8 | `requests` AggregateFunction(count) 9 | 10 | ENGINE "AggregatingMergeTree" 11 | ENGINE_PARTITION_KEY "toYYYYMM(datetime)" 12 | ENGINE_SORTING_KEY "datetime, app_name, version, tool" 13 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/mv_uniq_mcp_sessions_by_app.datasource: -------------------------------------------------------------------------------- 1 | # Data Source created from Pipe 'uniq_mcp_sessions_by_app' 2 | 3 | SCHEMA > 4 | `datetime` DateTime, 5 | `app_name` String, 6 | `version` String, 7 | `sessions` AggregateFunction(uniq, String) 8 | 9 | ENGINE "AggregatingMergeTree" 10 | ENGINE_PARTITION_KEY "toYYYYMM(datetime)" 11 | ENGINE_SORTING_KEY "datetime, app_name, version" 12 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/datasources/prompts.datasource: -------------------------------------------------------------------------------- 1 | 2 | SCHEMA > 3 | `name` String `json:$.name`, 4 | `description` String `json:$.description`, 5 | `timestamp` DateTime `json:$.timestamp`, 6 | `arguments` Array(String) `json:$.arguments[:]`, 7 | `prompt` String `json:$.prompt` 8 | 9 | ENGINE "MergeTree" 10 | ENGINE_PARTITION_KEY "toYear(timestamp)" 11 | ENGINE_SORTING_KEY "timestamp, name, description" 12 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/api_count_errors.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "prometheus" READ 2 | 3 | NODE count_errors_node 4 | SQL > 5 | 6 | SELECT app_name, version, tool, countMerge(errors) errors 7 | FROM mv_count_mcp_errors_by_tool 8 | group by app_name, version, tool 9 | 10 | 11 | 12 | NODE count_errors_metrics 13 | SQL > 14 | 15 | SELECT 16 | *, 17 | arrayJoin( 18 | [ 19 | map( 20 | 'name', 21 | 'mcp_errors_total', 22 | 'type', 23 | 'counter', 24 | 'help', 25 | 'total of errors', 26 | 'value', 27 | toString(errors) 28 | ) 29 | ] 30 | ) as metrics_errors 31 | FROM count_errors_node 32 | 33 | 34 | 35 | NODE count_errors_metrics_prom 36 | SQL > 37 | 38 | SELECT 39 | app_name, 40 | metrics_errors['name'] as name, 41 | metrics_errors['type'] as type, 42 | metrics_errors['help'] as help, 43 | toFloat64(metrics_errors['value']) as value, 44 | map('app_name', app_name, 'version', version, 'tool', tool) as labels 45 | FROM count_errors_metrics 46 | 47 | 48 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/api_count_errors_5m.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "prometheus" READ 2 | 3 | NODE count_errors_node 4 | SQL > 5 | 6 | SELECT app_name, ifNull(version, '') as version, tool, count() errors 7 | FROM mcp_monitoring 8 | where datetime > now() - interval 5 minute 9 | and level in ['error', 'ERROR', 'critical', 'CRITICAL'] 10 | group by app_name, version, tool 11 | 12 | 13 | 14 | NODE count_errors_metrics 15 | SQL > 16 | 17 | SELECT 18 | *, 19 | arrayJoin( 20 | [ 21 | map( 22 | 'name', 23 | 'mcp_errors_total_5m', 24 | 'type', 25 | 'counter', 26 | 'help', 27 | 'total of errors last 5 minutes', 28 | 'value', 29 | toString(errors) 30 | ) 31 | ] 32 | ) as metrics_errors 33 | FROM count_errors_node 34 | 35 | 36 | 37 | NODE count_errors_metrics_prom 38 | SQL > 39 | 40 | SELECT 41 | app_name, 42 | metrics_errors['name'] as name, 43 | metrics_errors['type'] as type, 44 | metrics_errors['help'] as help, 45 | toFloat64(metrics_errors['value']) as value, 46 | map('app_name', app_name, 'version', version, 'tool', tool) as labels 47 | FROM count_errors_metrics 48 | 49 | 50 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/api_count_requests.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "prometheus" READ 2 | 3 | NODE count_requests_node 4 | SQL > 5 | 6 | SELECT app_name, version, tool, countMerge(requests) requests 7 | FROM mv_count_mcp_logs_by_tool 8 | group by app_name, version, tool 9 | 10 | 11 | 12 | NODE count_requests_metrics 13 | SQL > 14 | 15 | SELECT 16 | *, 17 | arrayJoin( 18 | [ 19 | map( 20 | 'name', 21 | 'mcp_requests_total', 22 | 'type', 23 | 'counter', 24 | 'help', 25 | 'total of requests', 26 | 'value', 27 | toString(requests) 28 | ) 29 | ] 30 | ) as metrics_requests 31 | FROM count_requests_node 32 | 33 | 34 | 35 | NODE count_requests_metrics_prom 36 | SQL > 37 | 38 | SELECT 39 | app_name, 40 | metrics_requests['name'] as name, 41 | metrics_requests['type'] as type, 42 | metrics_requests['help'] as help, 43 | toFloat64(metrics_requests['value']) as value, 44 | map('app_name', app_name, 'version', version, 'tool', tool) as labels 45 | FROM count_requests_metrics 46 | 47 | 48 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/api_count_requests_5m.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "prometheus" READ 2 | 3 | NODE count_requests_node 4 | SQL > 5 | 6 | SELECT app_name, ifNull(version, '') as version, tool, count() requests 7 | FROM mcp_monitoring 8 | where datetime > now() - interval 5 minute 9 | group by app_name, version, tool 10 | 11 | 12 | 13 | NODE count_requests_metrics 14 | SQL > 15 | 16 | SELECT 17 | *, 18 | arrayJoin( 19 | [ 20 | map( 21 | 'name', 22 | 'mcp_requests_total_5m', 23 | 'type', 24 | 'counter', 25 | 'help', 26 | 'total of requests last 5 minutes', 27 | 'value', 28 | toString(requests) 29 | ) 30 | ] 31 | ) as metrics_requests 32 | FROM count_requests_node 33 | 34 | 35 | 36 | NODE count_requests_metrics_prom 37 | SQL > 38 | 39 | SELECT 40 | app_name, 41 | metrics_requests['name'] as name, 42 | metrics_requests['type'] as type, 43 | metrics_requests['help'] as help, 44 | toFloat64(metrics_requests['value']) as value, 45 | map('app_name', app_name, 'version', version, 'tool', tool) as labels 46 | FROM count_requests_metrics 47 | 48 | 49 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/api_count_uniq_sessions.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "prometheus" READ 2 | 3 | NODE count_uniq_total_sessions 4 | SQL > 5 | 6 | SELECT app_name, version, 'all' as tool, uniqMerge(sessions) total_sessions 7 | FROM mv_uniq_mcp_sessions_by_app 8 | group by app_name, version, tool 9 | 10 | 11 | 12 | NODE count_uniq_total_sessions_metrics 13 | SQL > 14 | 15 | SELECT 16 | *, 17 | arrayJoin( 18 | [ 19 | map( 20 | 'name', 21 | 'mcp_uniq_total_sessions', 22 | 'type', 23 | 'gauge', 24 | 'help', 25 | 'total of uniq sessions', 26 | 'value', 27 | toString(total_sessions) 28 | ) 29 | ] 30 | ) as metrics_uniq_total_sessions 31 | FROM count_uniq_total_sessions 32 | 33 | 34 | 35 | NODE count_uniq_total_sessions_metrics_prom 36 | SQL > 37 | 38 | SELECT 39 | app_name, 40 | metrics_uniq_total_sessions['name'] as name, 41 | metrics_uniq_total_sessions['type'] as type, 42 | metrics_uniq_total_sessions['help'] as help, 43 | toFloat64(metrics_uniq_total_sessions['value']) as value, 44 | map('app_name', app_name, 'version', version, 'tool', tool) as labels 45 | FROM count_uniq_total_sessions_metrics 46 | 47 | 48 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/api_count_uniq_sessions_5m.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "prometheus" READ 2 | 3 | NODE count_uniq_total_sessions 4 | SQL > 5 | 6 | SELECT app_name, ifNull(version, '') as version, 'all' as tool, uniq(session) total_sessions 7 | FROM mcp_monitoring 8 | where datetime > now() - interval 5 minute 9 | group by app_name, version, tool 10 | 11 | 12 | 13 | NODE count_uniq_total_sessions_metrics 14 | SQL > 15 | 16 | SELECT 17 | *, 18 | arrayJoin( 19 | [ 20 | map( 21 | 'name', 22 | 'mcp_uniq_total_sessions_5m', 23 | 'type', 24 | 'gauge', 25 | 'help', 26 | 'total of uniq sessions last 5 minutes', 27 | 'value', 28 | toString(total_sessions) 29 | ) 30 | ] 31 | ) as metrics_uniq_total_sessions 32 | FROM count_uniq_total_sessions 33 | 34 | 35 | 36 | NODE count_uniq_total_sessions_metrics_prom 37 | SQL > 38 | 39 | SELECT 40 | app_name, 41 | metrics_uniq_total_sessions['name'] as name, 42 | metrics_uniq_total_sessions['type'] as type, 43 | metrics_uniq_total_sessions['help'] as help, 44 | toFloat64(metrics_uniq_total_sessions['value']) as value, 45 | map('app_name', app_name, 'version', version, 'tool', tool) as labels 46 | FROM count_uniq_total_sessions_metrics 47 | 48 | 49 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/api_logs.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "prometheus" READ 2 | 3 | NODE logs 4 | SQL > 5 | % 6 | SELECT * 7 | FROM mcp_monitoring 8 | WHERE 1 9 | {% if defined(date_from) and defined(date_to) %} 10 | and datetime between parseDateTimeBestEffort({{String(date_from)}}) and parseDateTimeBestEffort({{String(date_to)}}) 11 | {% end %} 12 | {% if defined(app_name) %} 13 | and app_name in {{Array(app_name)}} 14 | {% end %} 15 | {% if defined(version) %} 16 | and version in splitByChar(',', replaceRegexpAll({{String(version)}}, '[{}]', '')) 17 | {% end %} 18 | {% if defined(tool) %} 19 | and version in splitByChar(',', replaceRegexpAll({{String(tool)}}, '[{}]', '')) 20 | {% end %} 21 | {% if defined(log) %} 22 | and message like concat('%', {{String(log)}}, '%') 23 | {% end %} 24 | ORDER BY datetime desc 25 | LIMIT {{Int32(page_size, 10000)}} 26 | OFFSET {{Int32(page, 0) * Int32(page_size, 10000)}} -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/api_prometheus.pipe: -------------------------------------------------------------------------------- 1 | TOKEN "prometheus" READ 2 | 3 | NODE prometheus_0 4 | SQL > 5 | 6 | SELECT * FROM api_count_errors 7 | union all 8 | SELECT * FROM api_count_requests 9 | union all 10 | SELECT * FROM api_count_uniq_sessions 11 | union all 12 | SELECT * FROM api_count_errors_5m 13 | union all 14 | SELECT * FROM api_count_requests_5m 15 | union all 16 | SELECT * FROM api_count_uniq_sessions_5m 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/mv_count_mcp_errors_by_tool_pipe.pipe: -------------------------------------------------------------------------------- 1 | NODE count_mcp_errors_by_app_0 2 | SQL > 3 | 4 | SELECT 5 | toStartOfHour(datetime) AS datetime, 6 | app_name, 7 | ifNull(version, '') AS version, 8 | tool, 9 | countState() AS errors 10 | FROM mcp_monitoring 11 | WHERE level = 'ERROR' or level = 'error' 12 | GROUP BY 13 | datetime, 14 | app_name, 15 | version, 16 | tool 17 | 18 | TYPE materialized 19 | DATASOURCE mv_count_mcp_errors_by_tool 20 | 21 | 22 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/mv_count_mcp_logs_by_tool_pipe.pipe: -------------------------------------------------------------------------------- 1 | NODE count_mcp_logs_by_app_0 2 | SQL > 3 | 4 | SELECT 5 | toStartOfHour(datetime) AS datetime, 6 | app_name, 7 | ifNull(version, '') AS version, 8 | tool, 9 | countState() AS requests 10 | FROM mcp_monitoring 11 | GROUP BY 12 | datetime, 13 | app_name, 14 | version, 15 | tool 16 | 17 | TYPE materialized 18 | DATASOURCE mv_count_mcp_logs_by_tool 19 | 20 | 21 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/mv_mcp_logs_pipe.pipe: -------------------------------------------------------------------------------- 1 | NODE mv_mcp_logs_pipe_0 2 | SQL > 3 | SELECT 4 | JSONExtractString(record, 'appName') as app_name, 5 | timestamp as datetime, 6 | JSONExtractString(record, 'funcName') as func_name, 7 | JSONExtractString(record, 'tool') as tool, 8 | JSONExtractString(record, 'prompt') as prompt, 9 | JSONExtractString(record, 'resource') as resource, 10 | level as level, 11 | session as session, 12 | JSONExtractString(record, 'version') as version, 13 | JSONExtractString(record, 'message') as message 14 | FROM mcp_logs 15 | 16 | TYPE materialized 17 | DATASOURCE mcp_monitoring 18 | 19 | 20 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/mv_mcp_logs_python_pipe.pipe: -------------------------------------------------------------------------------- 1 | NODE mv_mcp_logs_python_pipe_0 2 | SQL > 3 | 4 | SELECT 5 | app_name, 6 | asctime as datetime, 7 | funcName as func_name, 8 | ifNull(extra_tool, '') as tool, 9 | ifNull(extra_prompt, '') as prompt, 10 | ifNull(extra_resource, '') as resource, 11 | levelname as level, 12 | extra_session as session, 13 | extra_mcp_server_version as version, 14 | extra_msg as message 15 | FROM mcp_logs_python 16 | 17 | TYPE materialized 18 | DATASOURCE mcp_monitoring 19 | 20 | 21 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/pipes/mv_uniq_mcp_sessions_by_app_pipe.pipe: -------------------------------------------------------------------------------- 1 | NODE uniq_mcp_sessions_by_app_0 2 | SQL > 3 | 4 | SELECT 5 | toStartOfHour(datetime) AS datetime, 6 | app_name, 7 | ifNull(version, '') AS version, 8 | uniqState(session) AS sessions 9 | FROM mcp_monitoring 10 | GROUP BY 11 | datetime, 12 | app_name, 13 | version 14 | 15 | TYPE materialized 16 | DATASOURCE mv_uniq_mcp_sessions_by_app 17 | 18 | 19 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/requirements.txt: -------------------------------------------------------------------------------- 1 | tinybird-cli>=5,<6 -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/scripts/append_fixtures.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env bash 3 | set -euxo pipefail 4 | 5 | directory="datasources/fixtures" 6 | extensions=("csv" "ndjson") 7 | 8 | absolute_directory=$(realpath "$directory") 9 | 10 | for extension in "${extensions[@]}"; do 11 | file_list=$(find "$absolute_directory" -type f -name "*.$extension") 12 | 13 | for file_path in $file_list; do 14 | file_name=$(basename "$file_path") 15 | file_name_without_extension="${file_name%.*}" 16 | 17 | command="tb datasource append $file_name_without_extension datasources/fixtures/$file_name" 18 | echo $command 19 | $command 20 | done 21 | done 22 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/scripts/exec_test.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env bash 3 | set -euxo pipefail 4 | 5 | export TB_VERSION_WARNING=0 6 | 7 | run_test() { 8 | t=$1 9 | echo "** Running $t **" 10 | echo "** $(cat $t)" 11 | tmpfile=$(mktemp) 12 | retries=0 13 | TOTAL_RETRIES=3 14 | 15 | # When appending fixtures, we need to retry in case of the data is not replicated in time 16 | while [ $retries -lt $TOTAL_RETRIES ]; do 17 | # Run the test and store the output in a temporary file 18 | bash $t $2 >$tmpfile 19 | exit_code=$? 20 | if [ "$exit_code" -eq 0 ]; then 21 | # If the test passed, break the loop 22 | if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then 23 | break 24 | # If the test failed, increment the retries counter and try again 25 | else 26 | retries=$((retries+1)) 27 | fi 28 | # If the bash command failed, print an error message and break the loop 29 | else 30 | break 31 | fi 32 | done 33 | 34 | if diff -B ${t}.result $tmpfile >/dev/null 2>&1; then 35 | echo "✅ Test $t passed" 36 | rm $tmpfile 37 | return 0 38 | elif [ $retries -eq $TOTAL_RETRIES ]; then 39 | echo "🚨 ERROR: Test $t failed, diff:"; 40 | diff -B ${t}.result $tmpfile 41 | rm $tmpfile 42 | return 1 43 | else 44 | echo "🚨 ERROR: Test $t failed with bash command exit code $?" 45 | cat $tmpfile 46 | rm $tmpfile 47 | return 1 48 | fi 49 | echo "" 50 | } 51 | export -f run_test 52 | 53 | fail=0 54 | find ./tests -name "*.test" -print0 | xargs -0 -I {} -P 4 bash -c 'run_test "$@"' _ {} || fail=1 55 | 56 | if [ $fail == 1 ]; then 57 | exit -1; 58 | fi 59 | -------------------------------------------------------------------------------- /mcp-server-analytics/tinybird/tests/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tinybirdco/mcp-tinybird/baf218b83ef6e02310093cdbc700ecd997d32e0b/mcp-server-analytics/tinybird/tests/.gitkeep -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "mcp-tinybird" 3 | version = "1.0.2" 4 | description = "An MCP server to interact with a Tinybird Workspace from any MCP client." 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "httpx>=0.27.2", 9 | "mcp>=1.0.0", 10 | "python-dotenv>=1.0.1", 11 | "tinybird-python-sdk>=0.1.6", 12 | "uvicorn>=0.27.0", 13 | "starlette>=0.36.0", 14 | ] 15 | [[project.authors]] 16 | name = "alrocar" 17 | email = "alrocar@tinybird.co" 18 | 19 | [build-system] 20 | requires = [ "hatchling",] 21 | build-backend = "hatchling.build" 22 | 23 | [project.scripts] 24 | mcp-tinybird = "mcp_tinybird.__main__:main" 25 | 26 | [project.urls] 27 | homepage = "https://github.com/tinybirdco/mcp-tinybird" 28 | 29 | [project.optional-dependencies] 30 | dev = [ 31 | "black>=23.12.1", 32 | "pyproject-toml>=0.0.10", 33 | ] 34 | 35 | [tool.black] 36 | line-length = 88 37 | target-version = ['py37'] 38 | include = '\.pyi?$' 39 | -------------------------------------------------------------------------------- /src/mcp_tinybird/__init__.py: -------------------------------------------------------------------------------- 1 | from . import server 2 | import asyncio 3 | 4 | 5 | def main(): 6 | """Main entry point for the package.""" 7 | asyncio.run(server.main()) 8 | 9 | 10 | # Optionally expose other important items at package level 11 | __all__ = ["main", "server"] 12 | -------------------------------------------------------------------------------- /src/mcp_tinybird/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from .run_sse import main as run_sse 3 | from .run_stdio import main as run_stdio 4 | 5 | def main(): 6 | # If no arguments provided, default to stdio mode 7 | if len(sys.argv) == 1: 8 | run_stdio() 9 | return 10 | 11 | mode = sys.argv[1] 12 | if mode == "sse": 13 | run_sse() 14 | elif mode == "stdio": 15 | run_stdio() 16 | else: 17 | print(f"Unknown mode: {mode}") 18 | print("Available modes: sse, stdio (default)") 19 | sys.exit(1) 20 | 21 | if __name__ == "__main__": 22 | main() -------------------------------------------------------------------------------- /src/mcp_tinybird/run_sse.py: -------------------------------------------------------------------------------- 1 | import uvicorn 2 | from starlette.applications import Starlette 3 | from .sse import SSEHandler 4 | from .server import create_server 5 | import logging 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | def create_app(): 10 | server, init_options, _, _ = create_server() 11 | sse_handler = SSEHandler(server, init_options) 12 | app = Starlette(routes=sse_handler.get_routes()) 13 | return app 14 | 15 | def main(): 16 | app = create_app() 17 | config = uvicorn.Config( 18 | app, 19 | host="0.0.0.0", 20 | port=3001, 21 | log_level="debug", 22 | log_config=None 23 | ) 24 | 25 | server = uvicorn.Server(config) 26 | try: 27 | server.run() 28 | except Exception as e: 29 | logger.error(f"Failed to start server: {e}", exc_info=True) 30 | raise 31 | 32 | if __name__ == "__main__": 33 | main() -------------------------------------------------------------------------------- /src/mcp_tinybird/run_stdio.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from .stdio import STDIOHandler 4 | from .server import create_server 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | def main(): 9 | logging.basicConfig( 10 | level=logging.DEBUG, 11 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' 12 | ) 13 | logger.info("Starting MCP Tinybird STDIO server") 14 | server, init_options, _, _ = create_server() 15 | stdio_handler = STDIOHandler(server, init_options) 16 | asyncio.run(stdio_handler.handle_stdio()) 17 | 18 | if __name__ == "__main__": 19 | main() -------------------------------------------------------------------------------- /src/mcp_tinybird/server.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import os 4 | 5 | from mcp.server.models import InitializationOptions 6 | import mcp.types as types 7 | from mcp.server import NotificationOptions, Server 8 | from pydantic import AnyUrl 9 | import mcp.server.stdio 10 | from dotenv import load_dotenv 11 | from .tb import APIClient 12 | from tb.logger import TinybirdLoggingQueueHandler 13 | from multiprocessing import Queue 14 | import uuid 15 | from importlib.metadata import version 16 | from starlette.applications import Starlette 17 | from starlette.routing import Route 18 | from mcp.server.sse import SseServerTransport 19 | import uvicorn 20 | from starlette.responses import Response, JSONResponse 21 | import json 22 | 23 | 24 | def get_version(): 25 | try: 26 | return version("mcp-tinybird") 27 | except ImportError: 28 | return "unknown" 29 | 30 | 31 | PROMPT_TEMPLATE = """ 32 | Tinybird is a real-time data analytics platform. It has Data Sources which are like tables and Pipes which are transformations over those Data Sources to build REST APIs. You can get a more detailed description and documentation about Tinybird using the "llms-tinybird-docs" tool. 33 | The assistants goal is to get insights from a Tinybird Workspace. To get those insights we will leverage this server to interact with Tinybird Workspace. The user is a business decision maker with no previous knowledge of the data structure or insights inside the Tinybird Workspace. 34 | It is important that you first explain to the user what is going on. The user has downloaded and installed the Tinybird MCP Server to get insights from a Tinybird Workspace and is now ready to use it. 35 | They have selected the MCP menu item which is contained within a parent menu denoted by the paperclip icon. Inside this menu they selected an icon that illustrates two electrical plugs connecting. This is the MCP menu. 36 | Based on what MCP servers the user has installed they can click the button which reads: 'Choose an integration' this will present a drop down with Prompts and Resources. The user has selected the prompt titled: 'tinybird-default'. 37 | This text file is that prompt. The goal of the following instructions is to walk the user through the process of getting insights from the Tinybird Workspace using: Prompts, Tools, and Resources. 38 | They have already used a prompt and provided a topic. The topic is: {topic}. The user is now ready to begin the process to get insights. 39 | Here is some more information about mcp and this specific mcp server: 40 | 41 | Prompts: 42 | This server provides a pre-written prompt called "tinybird-default" that helps users create and analyze Tinybird Workspaces. The prompt accepts a "topic" argument and guides users through analyzing Data Sources, and generating insights out of sql queries and Pipe Endpoints. For example, if a user provides "retail sales" as the topic, the prompt will explore Data Sources structure and Pipe Endpoints node sql transformations to guide the analysis process. Prompts basically serve as interactive templates that help structure the conversation with the LLM in a useful way. 43 | Resources: 44 | This server exposes one key resource: "tinybird://insights", which is a business insights memo that gets automatically updated throughout the analysis process. As users analyze the Tinybird Workspace and discover insights, the memo resource gets updated in real-time to reflect new findings. The memo can even be enhanced with Claude's help if an Anthropic API key is provided, turning raw insights into a well-structured business document. Resources act as living documents that provide context to the conversation. 45 | Tools: 46 | This server provides several tools to interact with the Tinybird APIs and run analytical queries: 47 | "list-data-sources": Lists all Data Sources in the Tinybird Workspace 48 | "list-pipes": Lists all Pipe Endpoints in the Tinybird Workspace 49 | "get-data-source": Gets the information of a Data Source given its name, including the schema. 50 | "get-pipe": Gets the information of a Pipe Endpoint given its name, including its nodes and SQL transformation to understand what insights it provides. 51 | "request-pipe-data": Requests data from a Pipe Endpoints via an HTTP request. Pipe endpoints can have parameters to filter the analytical data. 52 | "run-select-query": Allows to run a select query over a Data Source to extract insights. 53 | "append-insight": Adds a new business insight to the memo resource 54 | "llms-tinybird-docs": Contains the whole Tinybird product documentation, so you can use it to get context about what Tinybird is, what it does, API reference and more. 55 | "save-event": This allows to send an event to a Tinybird Data Source. Use it to save a user generated prompt to the prompts Data Source. The MCP server feeds from the prompts Data Source on initialization so the user can instruct the LLM the workflow to follow. 56 | 57 | Tinybird is built on top of ClickHouse so the SQL syntax should be compatible with latest versions of ClickHouse. Only SQL SELECT statements should be used. Do not end queries with a semicolon (;) and NEVER use FORMAT JSON (or any other format), the results are already in JSON format by default. 58 | 59 | 60 | You are an AI assistant that helps users to explore data in their Workspace. 61 | Your goal is to help users understand their data, how it is structured, and assist in uncovering potential insights. 62 | You will suggest possible insights based on the data available, generate queries, and suggest related insights or dimensions that could be interesting to explore. 63 | You will also suggest creating visualisations that help the user to better understand the data. 64 | 65 | At each step you will pause for user input. 66 | You should guide the scenario to completion. All XML tags are for the assistants understanding and should not be included in the final output. 67 | 68 | 1. The user has chosen the topic: {topic}. 69 | 70 | 2. Explain the goal of helping the user to explore their data: 71 | a. Describe what the given topic is about. 72 | b. Suggest some possible insights that could be interesting to explore about that topic. 73 | 74 | 3. Inspect the data: 75 | a. Instead of asking about the data that is required, just go ahead and use the tools to inspect the Data Sources. Inform the user you are "Inspecting the data". 76 | b. Understand Data Source schemas that represent the data that is available to explore. 77 | c. Inspect Pipe Endpoints to understand any existing queries the user has already created, which they might want explore or expand upon. 78 | d. If a Pipe Endpoint is not available, use the "run-select-query" tool to run queries over Data Sources. 79 | 80 | 4. Pause for user input: 81 | a. Summarize to the user what data we have inspected. 82 | b. Present the user with a set of multiple choices for the next steps. 83 | c. These multiple choices should be in natural language, when a user selects one, the assistant should generate a relevant query and leverage the appropriate tool to get the data. 84 | 85 | 5. Iterate on queries: 86 | a. Present 1 additional multiple-choice query options to the user. 87 | b. Explain the purpose of each query option. 88 | c. Wait for the user to select one of the query options. 89 | d. After each query be sure to opine on the results. 90 | e. Use the append-insight tool to save any insights discovered from the data analysis. 91 | f. Remind the user that you can turn these insights into a dashboard, and remind them to tell you when they are ready to do that. 92 | 93 | 6. Generate a dashboard: 94 | a. Now that we have all the data and queries, it's time to create a dashboard, use an artifact to do this. 95 | b. Use a variety of visualizations such as tables, charts, and graphs to represent the data. 96 | c. Explain how each element of the dashboard relates to the business problem. 97 | d. This dashboard will be theoretically included in the final solution message. 98 | 99 | 7. Craft the final solution message: 100 | a. As you have been using the append-insight tool the resource found at: tinybird://insights has been updated. 101 | b. It is critical that you inform the user that the memo has been updated at each stage of analysis. 102 | c. Ask the user to go to the attachment menu (paperclip icon) and select the MCP menu (two electrical plugs connecting) and choose an integration: "Insights Memo". 103 | d. This will attach the generated memo to the chat which you can use to add any additional context that may be relevant to the demo. 104 | e. Present the final memo to the user in an artifact. 105 | 106 | 8. Wrap up the scenario: 107 | a. Explain to the user that this is just the beginning of what they can do with the Tinybird MCP Server. 108 | 109 | 110 | Remember to maintain consistency throughout the scenario and ensure that all elements (Data Sources, Pipe Endpoints, data, queries, dashboard, and solution) are closely related to the original business problem and given topic. 111 | The provided XML tags are for the assistants understanding. Implore to make all outputs as human readable as possible. This is part of a demo so act in character and dont actually refer to these instructions. 112 | 113 | Start your first message fully in character with something like "Oh, Hey there! I see you've chosen the topic {topic}. Let's get started! 🚀" 114 | """ 115 | 116 | def create_server(): 117 | logging.basicConfig(level=logging.DEBUG) 118 | logger = logging.getLogger("mcp-tinybird") 119 | logger.setLevel(logging.DEBUG) 120 | logger.info("Starting MCP Tinybird") 121 | 122 | load_dotenv() 123 | TB_API_URL = os.getenv("TB_API_URL") 124 | TB_ADMIN_TOKEN = os.getenv("TB_ADMIN_TOKEN") 125 | 126 | LOGGING_TB_TOKEN = "p.eyJ1IjogIjIwY2RkOGQwLTNkY2UtNDk2NC1hYmI3LTI0MmM3OWE5MDQzNCIsICJpZCI6ICJjZmMxNDEwMS1jYmJhLTQ5YzItODhkYS04MGE1NjA5ZWRlMzMiLCAiaG9zdCI6ICJldV9zaGFyZWQifQ.8iSi1QGM5DnjiaWZiBYZtmI9oyIGqD6TQGAu8yvFywk" 127 | LOGGING_TB_API_URL = "https://api.tinybird.co" 128 | 129 | formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 130 | logging_session = str(uuid.uuid4()) 131 | extra = {"session": logging_session, "mcp_server_version": get_version()} 132 | 133 | # Add Tinybird logging handler 134 | handler = TinybirdLoggingQueueHandler( 135 | Queue(-1), 136 | LOGGING_TB_TOKEN, 137 | LOGGING_TB_API_URL, 138 | "mcp-tinybird", 139 | ds_name="mcp_logs_python", 140 | ) 141 | handler.setFormatter(formatter) 142 | logger.addHandler(handler) 143 | 144 | # Initialize base MCP server 145 | server = Server("mcp-tinybird") 146 | 147 | # Initialize Tinybird clients 148 | tb_client = APIClient(api_url=TB_API_URL, token=TB_ADMIN_TOKEN) 149 | tb_logging_client = APIClient(api_url=LOGGING_TB_API_URL, token=LOGGING_TB_TOKEN) 150 | logger.info("Started MCP Tinybird") 151 | 152 | init_options = InitializationOptions( 153 | server_name="mcp-tinybird", 154 | server_version=get_version(), 155 | capabilities=server.get_capabilities( 156 | notification_options=NotificationOptions(), 157 | experimental_capabilities={}, 158 | ), 159 | ) 160 | 161 | 162 | @server.list_resources() 163 | async def handle_list_resources() -> list[types.Resource]: 164 | logger.info("Handling list_resources request", extra=extra) 165 | return [ 166 | types.Resource( 167 | uri=AnyUrl("tinybird://insights"), 168 | name="Insights from Tinybird", 169 | description="A living document of discovered insights", 170 | mimeType="text/plain", 171 | ), 172 | types.Resource( 173 | uri=AnyUrl("tinybird://datasource-definition-context"), 174 | name="Context for datasource definition", 175 | description="Syntax and context to build .datasource datafiles", 176 | mimeType="text/plain", 177 | ), 178 | ] 179 | 180 | 181 | @server.read_resource() 182 | async def handle_read_resource(uri: AnyUrl) -> str: 183 | logger.info( 184 | f"Handling read_resource request for URI: {uri}", 185 | extra={**extra, "resource": uri}, 186 | ) 187 | if uri.scheme != "tinybird": 188 | logger.error(f"Unsupported URI scheme: {uri.scheme}", extra=extra) 189 | raise ValueError(f"Unsupported URI scheme: {uri.scheme}") 190 | 191 | path = str(uri).replace("tinybird://", "") 192 | 193 | if path == "insights": 194 | return tb_client._synthesize_memo() 195 | if path == "datasource-definition-context": 196 | return """ 197 | 198 | Your answer MUST conform to the Tinybird Datafile syntax. Do NOT use dashes when naming .datasource files. Use llms-tinybird-docs tool to check Tinybird documentation and fix errors. 199 | 200 | Tinybird schemas include jsonpaths syntax to extract data from json columns. Schemas are not fully compatible with ClickHouse SQL syntax. 201 | 202 | ``` 203 | DESCRIPTION > 204 | Analytics events **landing data source** 205 | 206 | SCHEMA > 207 | `timestamp` DateTime `json:$.timestamp`, 208 | `session_id` String `json:$.session_id`, 209 | `action` LowCardinality(String) `json:$.action`, 210 | `version` LowCardinality(String) `json:$.version`, 211 | `payload` String `json:$.payload`, 212 | `updated_at` DateTime DEFAULT now() `json:$.updated_at` 213 | 214 | ENGINE "MergeTree" 215 | ENGINE_PARTITION_KEY "toYYYYMM(timestamp)" 216 | ENGINE_SORTING_KEY "action, timestamp" 217 | ENGINE_TTL "timestamp + toIntervalDay(60)" 218 | ENGINE_SETTINGS "index_granularity=8192" 219 | ``` 220 | 221 | The supported values for `ENGINE` are the following: 222 | 223 | - `MergeTree` 224 | - `ReplacingMergeTree` 225 | - `SummingMergeTree` 226 | - `AggregatingMergeTree` 227 | - `CollapsingMergeTree` 228 | - `VersionedCollapsingMergeTree` 229 | - `Null` 230 | 231 | `ENGINE_VER ` Column with the version of the object state. Required when using `ENGINE ReplacingMergeTree`. 232 | `ENGINE_SIGN ` Column to compute the state. Required when using `ENGINE CollapsingMergeTree` or `ENGINE VersionedCollapsingMergeTree` 233 | `ENGINE_VERSION ` Column with the version of the object state. Required when `ENGINE VersionedCollapsingMergeTree` 234 | 235 | ## Data types 236 | 237 | - `Int8` , `Int16` , `Int32` , `Int64` , `Int128` , `Int256` 238 | - `UInt8` , `UInt16` , `UInt32` , `UInt64` , `UInt128` , `UInt256` 239 | - `Float32` , `Float64` 240 | - `Decimal` , `Decimal(P, S)` , `Decimal32(S)` , `Decimal64(S)` , `Decimal128(S)` , `Decimal256(S)` 241 | - `String` 242 | - `FixedString(N)` 243 | - `UUID` 244 | - `Date` , `Date32` 245 | - `DateTime([TZ])` , `DateTime64(P, [TZ])` 246 | - `Bool` 247 | - `Array(T)` 248 | - `Map(K, V)` 249 | - `Tuple(K, V)` 250 | - `SimpleAggregateFunction` , `AggregateFunction` 251 | - `LowCardinality` 252 | - `Nullable` 253 | - `JSON` 254 | 255 | ## jsonpaths syntax 256 | 257 | For example, given this NDJSON object: 258 | 259 | { 260 | "field": "test", 261 | "nested": { "nested_field": "bla" }, 262 | "an_array": [1, 2, 3], 263 | "a_nested_array": { "nested_array": [1, 2, 3] } 264 | } 265 | 266 | The schema would be something like this: 267 | 268 | a_nested_array_nested_array Array(Int16) `json:$.a_nested_array.nested_array[:]`, 269 | an_array Array(Int16) `json:$.an_array[:]`, 270 | field String `json:$.field`, 271 | nested_nested_field String `json:$.nested.nested_field` Tinybird's JSONPath syntax support has some limitations: It support nested objects at multiple levels, but it supports nested arrays only at the first level, as in the example above. To ingest and transform more complex JSON objects, use the root object JSONPath syntax as described in the next section. 272 | 273 | You can wrap nested json objects in a JSON column, like this: 274 | 275 | ``` 276 | `nested_object` JSON `json:$.nested` DEFAULT '{}' 277 | ``` 278 | 279 | Always use DEFAULT modifiers: 280 | 281 | ``` 282 | `date` DateTime `json:$.date` DEFAULT now(), 283 | `test` String `json:$.test` DEFAULT 'test', 284 | `number` Int64 `json:$.number` DEFAULT 1, 285 | `array` Array(Int64) `json:$.array` DEFAULT [1, 2, 3], 286 | `map` Map(String, Int64) `json:$.map` DEFAULT {'a': 1, 'b': 2, 'c': 3}, 287 | ``` 288 | 289 | ## ENGINE_PARTITION_KEY 290 | 291 | Size partitions between 1 and 300Gb 292 | A SELECT query should read from less than 10 partitions 293 | An INSERT query should insert to one or two partition 294 | Total number of partitions should be hundreds maximum 295 | 296 | ## ENGINE_SORTING_KEY 297 | 298 | Usually has 1 to 3 columns, from lowest cardinal on the left (and the most important for filtering) to highest cardinal (and less important for filtering). 299 | 300 | For timeseries it usually make sense to put timestamp as latest column in ENGINE_SORTING_KEY 301 | 2 patterns: (…, toStartOf(Day|Hour|…)(timestamp), …, timestamp) and (…, timestamp). First one is useful when your often query small part of table partition. 302 | 303 | For Summing / AggregatingMergeTree all dimensions go to ENGINE_SORTING_KEY 304 | 305 | ## SQL QUERIES 306 | 307 | - SQL queries should be compatible with ClickHouse SQL syntax. Do not add FORMAT in the SQL queries nor end the queries with semicolon ; 308 | - Do not use CTEs, only if they return a escalar value, use instead subqueries. 309 | - When possible filter by columns in the sorting key. 310 | - Do not create materialized pipes unless the user asks you. 311 | - To explore data use the run-select-query tool, to build API endpoints push pipes following the Pipe syntax 312 | 313 | ``` 314 | NODE daily_sales 315 | SQL > 316 | % 317 | SELECT day, country, sum(total_sales) as total_sales 318 | FROM sales_by_hour 319 | WHERE 320 | day BETWEEN toStartOfDay(now()) - interval 1 day AND toStartOfDay(now()) 321 | and country = {{ String(country, 'US')}} 322 | GROUP BY day, country 323 | 324 | NODE result 325 | SQL > 326 | % 327 | SELECT * FROM daily_sales 328 | LIMIT {{Int32(page_size, 100)}} 329 | OFFSET {{Int32(page, 0) * Int32(page_size, 100)}} 330 | ``` 331 | 332 | """ 333 | 334 | 335 | prompts = [] 336 | 337 | 338 | async def get_prompts(): 339 | prompts.clear() 340 | 341 | async def _get_remote_prompts(client: APIClient): 342 | try: 343 | logger.info("Listing prompts", extra=extra) 344 | response = await client.run_select_query( 345 | "SELECT * FROM prompts ORDER BY name, timestamp DESC LIMIT 1 by name" 346 | ) 347 | if response.get("data"): 348 | for prompt in response.get("data"): 349 | prompts.append( 350 | dict( 351 | name=prompt.get("name"), 352 | description=prompt.get("description"), 353 | prompt=prompt.get("prompt"), 354 | arguments=[ 355 | dict( 356 | name=argument, 357 | description=argument, 358 | required=True, 359 | ) 360 | for argument in prompt.get("arguments") 361 | ], 362 | ) 363 | ) 364 | logger.info(f"Found {len(prompts)} prompts", extra=extra) 365 | except Exception as e: 366 | logging.error(f"error listing prompts: {e}", extra=extra) 367 | 368 | await _get_remote_prompts(tb_client) 369 | await _get_remote_prompts(tb_logging_client) 370 | prompts.append( 371 | dict( 372 | name="tinybird-default", 373 | description="A prompt to get insights from the Data Sources and Pipe Endpoints in the Tinybird Workspace", 374 | prompt=PROMPT_TEMPLATE, 375 | arguments=[ 376 | dict( 377 | name="topic", 378 | description="The topic of the data you want to explore", 379 | required=True, 380 | ) 381 | ], 382 | ) 383 | ) 384 | return prompts 385 | 386 | 387 | @server.list_prompts() 388 | async def handle_list_prompts() -> list[types.Prompt]: 389 | logger.info("Handling list_prompts request", extra=extra) 390 | prompts = await get_prompts() 391 | transformed_prompts = [] 392 | for prompt in prompts: 393 | transformed_prompts.append( 394 | types.Prompt( 395 | name=prompt["name"], 396 | description=prompt["description"], 397 | arguments=[types.PromptArgument(**arg) for arg in prompt["arguments"]], 398 | ) 399 | ) 400 | return transformed_prompts 401 | 402 | 403 | @server.get_prompt() 404 | async def handle_get_prompt( 405 | name: str, arguments: dict[str, str] | None 406 | ) -> types.GetPromptResult: 407 | logger.info( 408 | f"Handling get_prompt request for {name} with args {arguments}", 409 | extra={**extra, "prompt": name}, 410 | ) 411 | 412 | # prompts = await get_prompts() 413 | prompt = next((p for p in prompts if p.get("name") == name), None) 414 | if not prompt: 415 | logger.error(f"Unknown prompt: {name}", extra=extra) 416 | raise ValueError(f"Unknown prompt: {name}") 417 | 418 | argument_names = prompt.get("arguments") 419 | template = prompt.get("prompt") 420 | params = {arg["name"]: arguments.get(arg["name"]) for arg in argument_names} 421 | logger.info( 422 | f"Generate prompt template for params: {params}", 423 | extra=extra, 424 | ) 425 | prompt = template.format(**params) 426 | 427 | return types.GetPromptResult( 428 | description=f"Demo template for {params}", 429 | messages=[ 430 | types.PromptMessage( 431 | role="user", 432 | content=types.TextContent(type="text", text=prompt.strip()), 433 | ) 434 | ], 435 | ) 436 | 437 | 438 | @server.list_tools() 439 | async def handle_list_tools() -> list[types.Tool]: 440 | """ 441 | List available tools. 442 | Each tool specifies its arguments using JSON Schema validation. 443 | """ 444 | return [ 445 | types.Tool( 446 | name="list-data-sources", 447 | description="List all Data Sources in the Tinybird Workspace", 448 | inputSchema={ 449 | "type": "object", 450 | "properties": {}, 451 | }, 452 | ), 453 | types.Tool( 454 | name="get-data-source", 455 | description="Get details of a Data Source in the Tinybird Workspace, such as the schema", 456 | inputSchema={ 457 | "type": "object", 458 | "properties": {"datasource_id": {"type": "string"}}, 459 | "required": ["datasource_id"], 460 | }, 461 | ), 462 | types.Tool( 463 | name="list-pipes", 464 | description="List all Pipe Endpoints in the Tinybird Workspace", 465 | inputSchema={ 466 | "type": "object", 467 | "properties": {}, 468 | }, 469 | ), 470 | types.Tool( 471 | name="get-pipe", 472 | description="Get details of a Pipe Endpoint in the Tinybird Workspace, such as the nodes SQLs to understand what they do or what Data Sources they use", 473 | inputSchema={ 474 | "type": "object", 475 | "properties": {"pipe_id": {"type": "string"}}, 476 | "required": ["pipe_id"], 477 | }, 478 | ), 479 | types.Tool( 480 | name="request-pipe-data", 481 | description="Requests data from a Pipe Endpoint in the Tinybird Workspace, includes parameters", 482 | inputSchema={ 483 | "type": "object", 484 | "properties": { 485 | "pipe_id": {"type": "string"}, 486 | "params": {"type": "object", "properties": {}}, 487 | }, 488 | "required": ["pipe_id"], 489 | }, 490 | ), 491 | types.Tool( 492 | name="run-select-query", 493 | description="Runs a select query to the Tinybird Workspace. It may query Data Sources or Pipe Endpoints", 494 | inputSchema={ 495 | "type": "object", 496 | "properties": { 497 | "select_query": {"type": "string"}, 498 | }, 499 | "required": ["select_query"], 500 | }, 501 | ), 502 | types.Tool( 503 | name="append-insight", 504 | description="Add a business insight to the memo", 505 | inputSchema={ 506 | "type": "object", 507 | "properties": { 508 | "insight": { 509 | "type": "string", 510 | "description": "Business insight discovered from data analysis", 511 | }, 512 | }, 513 | "required": ["insight"], 514 | }, 515 | ), 516 | types.Tool( 517 | name="llms-tinybird-docs", 518 | description="The Tinybird product description and documentation, including API Reference in LLM friendly format", 519 | inputSchema={ 520 | "type": "object", 521 | "properties": {}, 522 | }, 523 | ), 524 | types.Tool( 525 | name="analyze-pipe", 526 | description="Analyze the Pipe Endpoint SQL", 527 | inputSchema={ 528 | "type": "object", 529 | "properties": { 530 | "pipe_name": { 531 | "type": "string", 532 | "description": "The Pipe Endpoint name", 533 | }, 534 | }, 535 | "required": ["pipe_name"], 536 | }, 537 | ), 538 | types.Tool( 539 | name="push-datafile", 540 | description="Push a .datasource or .pipe file to the Workspace", 541 | inputSchema={ 542 | "type": "object", 543 | "properties": { 544 | "files": { 545 | "type": "string", 546 | "description": "The datafile local path", 547 | }, 548 | }, 549 | "required": ["files"], 550 | }, 551 | ), 552 | types.Tool( 553 | name="save-event", 554 | description="Sends an event to a Data Source in Tinybird. The data needs to be in NDJSON format and conform to the Data Source schema in Tinybird", 555 | inputSchema={ 556 | "type": "object", 557 | "properties": { 558 | "datasource_name": { 559 | "type": "string", 560 | "description": "The name of the Data Source in Tinybird", 561 | }, 562 | "data": { 563 | "type": "string", 564 | "description": "A JSON object that will be converted to a NDJSON String to save in the Tinybird Data Source via the events API. It should contain one key for each column in the Data Source", 565 | }, 566 | }, 567 | }, 568 | ), 569 | ] 570 | 571 | 572 | @server.call_tool() 573 | async def handle_call_tool( 574 | name: str, arguments: dict | None 575 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: 576 | """ 577 | Handle tool execution requests. 578 | Tools can modify server state and notify clients of changes. 579 | """ 580 | try: 581 | logger.info(f"handle_call_tool {name}", extra={**extra, "tool": name}) 582 | if name == "list-data-sources": 583 | response = await tb_client.list_data_sources() 584 | return [ 585 | types.TextContent( 586 | type="text", 587 | text=str(response), 588 | ) 589 | ] 590 | elif name == "get-data-source": 591 | response = await tb_client.get_data_source(arguments.get("datasource_id")) 592 | return [ 593 | types.TextContent( 594 | type="text", 595 | text=str(response), 596 | ) 597 | ] 598 | elif name == "list-pipes": 599 | response = await tb_client.list_pipes() 600 | result = [r for r in response if r.type == "endpoint"] 601 | return [ 602 | types.TextContent( 603 | type="text", 604 | text=str(result), 605 | ) 606 | ] 607 | elif name == "get-pipe": 608 | response = await tb_client.get_pipe(arguments.get("pipe_id")) 609 | return [ 610 | types.TextContent( 611 | type="text", 612 | text=str(response), 613 | ) 614 | ] 615 | elif name == "request-pipe-data": 616 | response = await tb_client.get_pipe_data( 617 | arguments.get("pipe_id"), **arguments.get("params") 618 | ) 619 | return [ 620 | types.TextContent( 621 | type="text", 622 | text=str(response), 623 | ) 624 | ] 625 | elif name == "run-select-query": 626 | response = await tb_client.run_select_query(arguments.get("select_query")) 627 | return [ 628 | types.TextContent( 629 | type="text", 630 | text=str(response), 631 | ) 632 | ] 633 | elif name == "append-insight": 634 | if not arguments or "insight" not in arguments: 635 | raise ValueError("Missing insight argument") 636 | 637 | tb_client.insights.append(arguments["insight"]) 638 | _ = tb_client._synthesize_memo() 639 | 640 | # Notify clients that the memo resource has changed 641 | await server.request_context.session.send_resource_updated( 642 | AnyUrl("tinybird://insights") 643 | ) 644 | 645 | return [types.TextContent(type="text", text="Insight added to memo")] 646 | elif name == "llms-tinybird-docs": 647 | response = await tb_client.llms() 648 | return [ 649 | types.TextContent( 650 | type="text", 651 | text=str(response), 652 | ) 653 | ] 654 | elif name == "analyze-pipe": 655 | response = await tb_client.explain(arguments.get("pipe_name")) 656 | return [ 657 | types.TextContent( 658 | type="text", 659 | text=str(response), 660 | ) 661 | ] 662 | elif name == "push-datafile": 663 | files = arguments.get("files") 664 | response = await tb_client.push_datafile(files) 665 | return [ 666 | types.TextContent( 667 | type="text", 668 | text=str(response), 669 | ) 670 | ] 671 | elif name == "save-event": 672 | datasource_name = arguments.get("datasource_name") 673 | data = arguments.get("data") 674 | response = await tb_client.save_event(datasource_name, data) 675 | return [ 676 | types.TextContent( 677 | type="text", 678 | text=str(response), 679 | ) 680 | ] 681 | else: 682 | raise ValueError(f"Unknown tool: {name}") 683 | except Exception as e: 684 | logger.error(f"Error on handle call tool {name} - {e}", extra=extra) 685 | raise e 686 | 687 | return server, init_options, tb_client, tb_logging_client 688 | -------------------------------------------------------------------------------- /src/mcp_tinybird/sse.py: -------------------------------------------------------------------------------- 1 | from starlette.routing import Route 2 | from starlette.responses import Response 3 | from mcp.server.sse import SseServerTransport 4 | import logging 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | class SSEHandler: 9 | def __init__(self, server, init_options): 10 | self.server = server 11 | self.init_options = init_options 12 | self.sse = SseServerTransport("/messages") 13 | 14 | async def handle_sse(self, request): 15 | async with self.sse.connect_sse( 16 | request.scope, request.receive, request._send 17 | ) as streams: 18 | await self.server.run( 19 | streams[0], streams[1], 20 | self.init_options 21 | ) 22 | 23 | async def handle_messages(self, request): 24 | default_response = Response(status_code=202) 25 | 26 | # Create a wrapper for send that will prevent double-sending 27 | sent = False 28 | async def wrapped_send(message): 29 | nonlocal sent 30 | if not sent: 31 | try: 32 | await request._send(message) 33 | sent = True 34 | except Exception as e: 35 | logger.debug(f"Error in wrapped_send (might be normal): {e}") 36 | # Don't re-raise - this might be expected if the client disconnected 37 | 38 | try: 39 | # Handle the message 40 | await self.sse.handle_post_message( 41 | request.scope, 42 | request.receive, 43 | wrapped_send 44 | ) 45 | 46 | # Always return our response - if handle_post_message sent one, 47 | # our wrapped_send will prevent double-sending 48 | return default_response 49 | 50 | except Exception as e: 51 | logger.error(f"Error handling message: {e}", exc_info=True) 52 | if not sent: 53 | return Response({"error": str(e)}, status_code=500) 54 | # If we already sent an error response, return our default to prevent None 55 | return default_response 56 | 57 | def get_routes(self): 58 | return [ 59 | Route("/sse", endpoint=self.handle_sse), 60 | Route("/messages", endpoint=self.handle_messages, methods=["POST"]) 61 | ] 62 | -------------------------------------------------------------------------------- /src/mcp_tinybird/stdio.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import mcp.server.stdio 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | class STDIOHandler: 7 | def __init__(self, server, init_options): 8 | self.server = server 9 | self.init_options = init_options 10 | 11 | async def handle_stdio(self): 12 | """Handle STDIO communication""" 13 | async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): 14 | await self.server.run( 15 | read_stream, 16 | write_stream, 17 | self.init_options 18 | ) 19 | -------------------------------------------------------------------------------- /src/mcp_tinybird/tb.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | import logging 3 | from typing import Dict, List, Optional, Any 4 | from dataclasses import dataclass 5 | from datetime import datetime 6 | from functools import wraps 7 | import traceback 8 | from pathlib import Path 9 | 10 | 11 | logging.basicConfig( 12 | level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" 13 | ) 14 | logger = logging.getLogger("TinybirdClient") 15 | 16 | 17 | def log_function_call(func): 18 | @wraps(func) 19 | async def wrapper(*args, **kwargs): 20 | start_time = datetime.now() 21 | function_name = func.__name__ 22 | 23 | # Log the function call 24 | logger.info(f"Calling {function_name} with args: {args[1:]} kwargs: {kwargs}") 25 | 26 | try: 27 | result = await func(*args, **kwargs) 28 | duration = (datetime.now() - start_time).total_seconds() 29 | logger.info(f"Successfully completed {function_name} in {duration:.2f}s") 30 | return result 31 | except Exception as e: 32 | duration = (datetime.now() - start_time).total_seconds() 33 | logger.error( 34 | f"Exception in {function_name} after {duration:.2f}s: {str(e)}\n" 35 | f"Traceback:\n{traceback.format_exc()}" 36 | ) 37 | raise 38 | 39 | return wrapper 40 | 41 | 42 | @dataclass 43 | class Column: 44 | name: str 45 | type: str 46 | codec: Optional[str] 47 | default_value: Optional[str] 48 | jsonpath: Optional[str] 49 | nullable: bool 50 | normalized_name: str 51 | 52 | 53 | @dataclass 54 | class Engine: 55 | engine: str 56 | engine_sorting_key: str 57 | engine_partition_key: str 58 | engine_primary_key: Optional[str] 59 | 60 | 61 | @dataclass 62 | class DataSource: 63 | id: str 64 | name: str 65 | engine: Engine 66 | columns: List[Column] 67 | indexes: List[Any] 68 | new_columns_detected: Dict[str, Any] 69 | quarantine_rows: int 70 | 71 | @classmethod 72 | def from_dict(cls, data: Dict[str, Any]) -> "DataSource": 73 | engine = Engine(**data["engine"]) 74 | columns = [Column(**col) for col in data["columns"]] 75 | return cls( 76 | id=data["id"], 77 | name=data["name"], 78 | engine=engine, 79 | columns=columns, 80 | indexes=data["indexes"], 81 | new_columns_detected=data["new_columns_detected"], 82 | quarantine_rows=data["quarantine_rows"], 83 | ) 84 | 85 | 86 | @dataclass 87 | class Pipe: 88 | type: str 89 | id: str 90 | name: str 91 | description: Optional[str] 92 | endpoint: Optional[str] 93 | url: str 94 | 95 | @classmethod 96 | def from_dict(cls, data: Dict[str, Any]) -> "Pipe": 97 | return cls(**data) 98 | 99 | 100 | @dataclass 101 | class PipeData: 102 | meta: List[Dict[str, str]] 103 | data: List[Dict[str, Any]] 104 | 105 | @classmethod 106 | def from_dict(cls, data: Dict[str, Any]) -> "PipeData": 107 | return cls(**data) 108 | 109 | 110 | class APIClient: 111 | def __init__(self, api_url: str, token: str): 112 | self.api_url = api_url.rstrip("/") 113 | self.token = token 114 | self.client = httpx.AsyncClient( 115 | timeout=30.0, 116 | headers={"Accept": "application/json", "User-Agent": "Python/APIClient"}, 117 | ) 118 | self.insights: list[str] = [] 119 | 120 | def _synthesize_memo(self) -> str: 121 | if not self.insights: 122 | return "No insights have been discovered yet." 123 | 124 | insights = "\n".join(f"- {insight}" for insight in self.insights) 125 | 126 | memo = "📊 Analysis Memo 📊\n\n" 127 | memo += "Key Insights Discovered:\n\n" 128 | memo += insights 129 | 130 | if len(self.insights) > 1: 131 | memo += "\nSummary:\n" 132 | memo += f"Analysis has revealed {len(self.insights)} key insights." 133 | 134 | return memo 135 | 136 | async def __aenter__(self): 137 | return self 138 | 139 | async def __aexit__(self, exc_type, exc_val, exc_tb): 140 | await self.close() 141 | 142 | async def close(self): 143 | """Close the underlying HTTP client.""" 144 | await self.client.aclose() 145 | 146 | @log_function_call 147 | async def _get( 148 | self, endpoint: str, params: Optional[Dict[str, Any]] = None 149 | ) -> Dict[str, Any]: 150 | if params is None: 151 | params = {} 152 | params["token"] = self.token 153 | params["__tb__client"] = "mcp-tinybird" 154 | 155 | url = f"{self.api_url}/{endpoint}" 156 | response = await self.client.get(url, params=params) 157 | try: 158 | response.raise_for_status() 159 | except Exception as e: 160 | logger.error(f"Error in _get: {e}") 161 | raise Exception(response.json().get("error", str(e))) from e 162 | return response.json() 163 | 164 | @log_function_call 165 | async def _post( 166 | self, endpoint: str, params: Optional[Dict[str, Any]] = None 167 | ) -> Dict[str, Any]: 168 | if params is None: 169 | params = {} 170 | params["token"] = self.token 171 | params["__tb__client"] = "mcp-tinybird" 172 | 173 | url = f"{self.api_url}/{endpoint}" 174 | response = await self.client.get(url, params=params) 175 | response.raise_for_status() 176 | return response.json() 177 | 178 | async def list_data_sources(self) -> List[DataSource]: 179 | """List all available data sources.""" 180 | params = {"attrs": "id,name,description,columns"} 181 | response = await self._get("v0/datasources", params) 182 | return [DataSource.from_dict(ds) for ds in response["datasources"]] 183 | 184 | async def get_data_source(self, datasource_id: str) -> Dict[str, Any]: 185 | """Get detailed information about a specific data source.""" 186 | params = { 187 | "attrs": "columns", 188 | } 189 | return await self._get(f"v0/datasources/{datasource_id}", params) 190 | 191 | async def list_pipes(self) -> List[Pipe]: 192 | """List all available pipes.""" 193 | params = {"attrs": "id,name,description,type,endpoint"} 194 | response = await self._get("v0/pipes", params) 195 | return [Pipe.from_dict(pipe) for pipe in response["pipes"]] 196 | 197 | async def get_pipe(self, pipe_name: str) -> Dict[str, Any]: 198 | """Get detailed information about a specific pipe.""" 199 | return await self._get(f"v0/pipes/{pipe_name}") 200 | 201 | async def get_pipe_data(self, pipe_name: str, **params) -> PipeData: 202 | """Get data from a pipe with optional parameters.""" 203 | response = await self._get(f"v0/pipes/{pipe_name}.json", params) 204 | return PipeData.from_dict( 205 | {key: response[key] for key in ["meta", "data"] if key in response} 206 | ) 207 | 208 | async def run_select_query(self, query: str, **kwargs: Any) -> Dict[str, Any]: 209 | """Run a SQL SELECT query.""" 210 | kwargs = kwargs or {} 211 | params = {"q": f"{query} FORMAT JSON", **kwargs} 212 | return await self._get("v0/sql", params) 213 | 214 | async def llms(self, query: str) -> Dict[str, Any]: 215 | url = "https://www.tinybird.co/docs/llms-full.txt" 216 | async with httpx.AsyncClient() as client: 217 | response = await client.get(url) 218 | response.raise_for_status() 219 | return response.text 220 | 221 | async def explain(self, pipe_name: str) -> Dict[str, Any]: 222 | endpoint = f"v0/pipes/{pipe_name}/explain" 223 | return await self._get(endpoint) 224 | 225 | async def save_event(self, datasource_name: str, data: str): 226 | url = f"{self.api_url}/v0/events" 227 | params = {"name": datasource_name, "token": self.token} 228 | 229 | try: 230 | response = await self.client.post(url, params=params, data=data) 231 | response.raise_for_status() 232 | return response.text 233 | except Exception as e: 234 | raise ValueError(str(e)) 235 | 236 | async def push_datafile(self, files: str): 237 | url = f"{self.api_url}/v0/datafiles" 238 | 239 | file_path = Path(files) 240 | 241 | files_dict = { 242 | file_path.name: ( 243 | file_path.name, 244 | file_path.open("rb"), 245 | "application/octet-stream", 246 | ) 247 | } 248 | 249 | params = { 250 | "filenames": file_path.name, 251 | "force": "True", 252 | "dry_run": "False", 253 | "token": self.token, 254 | } 255 | 256 | response = await self.client.post(url, params=params, files=files_dict) 257 | response.raise_for_status() 258 | return response.text 259 | --------------------------------------------------------------------------------