diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 50aa3b0..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Release Notecard and Notehub MCP servers - -on: - release: - types: [published] - workflow_dispatch: - -jobs: - goreleaser: - runs-on: macos-latest - steps: - - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Fetch all tags - run: git fetch --force --tags - - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: 1.21 - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser - version: latest - args: release -f .goreleaser.yaml --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }} - - - name: Upload assets - uses: actions/upload-artifact@v4 - with: - name: note-mcp - path: dist/* diff --git a/.goreleaser.yml b/.goreleaser.yml deleted file mode 100644 index 6f4d33b..0000000 --- a/.goreleaser.yml +++ /dev/null @@ -1,95 +0,0 @@ -version: 2 - -before: - hooks: - - go mod tidy - - go generate ./... - -builds: - - - id: "notecard-mcp-linux" - dir: notecard - binary: notecard-mcp - - env: - - CGO_ENABLED=0 - goos: - - linux - goarch: - - amd64 - - arm - - arm64 - - - - id: "notehub-mcp-linux" - dir: notehub - binary: notehub-mcp - - env: - - CGO_ENABLED=0 - goos: - - linux - goarch: - - amd64 - - arm - - arm64 - - - id: "notecard-mcp" - dir: notecard - binary: notecard-mcp - - env: - - CGO_ENABLED=1 - goos: - - windows - - darwin - goarch: - - amd64 - - arm64 - ignore: - - goos: windows - goarch: arm - goarm: 6 - - goos: windows - goarch: arm64 - - - - id: "notehub-mcp" - dir: notehub - binary: notehub-mcp - - env: - - CGO_ENABLED=1 - goos: - - windows - - darwin - goarch: - - amd64 - - arm64 - ignore: - - goos: windows - goarch: arm - goarm: 6 - - goos: windows - goarch: arm64 - -checksum: - name_template: 'checksums.txt' -changelog: - sort: asc - filters: - exclude: - - '^docs:' - - '^test:' - -brews: - - repository: - owner: blues - name: homebrew-note-mcp - token: "{{ .Env.TAP_GITHUB_TOKEN }}" - directory: Formula - homepage: https://blues.com - description: Note MCP servers - license: MIT - dependencies: - - name: note-cli diff --git a/Makefile b/Makefile index 6888bb1..7021051 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Makefile for note-mcp project -.PHONY: all clean build test notecard notehub blues-expert inspect inspect-notecard inspect-notehub inspect-blues-expert help docs docs-notehub docs-notecard docs-blues-expert fmt vet develop +.PHONY: all clean build test blues-expert inspect-blues-expert help docs docs-blues-expert fmt vet develop # Go build flags GO=go @@ -8,8 +8,6 @@ GOFLAGS=-v LDFLAGS=-w -s # Project directories -NOTECARD_DIR=./notecard -NOTEHUB_DIR=./notehub BLUES_EXPERT_DIR=./blues-expert # Default target @@ -21,30 +19,14 @@ help: ## Show this help message @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-20s\033[0m %s\n", $$1, $$2}' # Build targets -build: notecard notehub blues-expert ## Build all MCP servers +build: blues-expert ## Build all MCP servers @echo "✓ Build complete" # Build targets for each MCP server -notecard: ## Build the Notecard MCP server - @echo "Building notecard..." - cd $(NOTECARD_DIR) && $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o notecard - -notehub: ## Build the Notehub MCP server - @echo "Building notehub..." - cd $(NOTEHUB_DIR) && $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o notehub - blues-expert: ## Build the Blues Expert MCP server @echo "Building blues-expert..." cd $(BLUES_EXPERT_DIR) && $(GO) build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o blues-expert -inspect-notecard: notecard ## Run MCP inspector for notecard - @echo "Starting MCP inspector for notecard..." - npx @modelcontextprotocol/inspector --config mcp.json --server notecard - -inspect-notehub: notehub ## Run MCP inspector for notehub - @echo "Starting MCP inspector for notehub..." - npx @modelcontextprotocol/inspector --config mcp.json --server notehub - inspect-blues-expert: blues-expert ## Run MCP inspector for blues-expert @echo "Starting MCP inspector for blues-expert..." npx @modelcontextprotocol/inspector --config mcp.json --server blues-expert @@ -54,19 +36,7 @@ inspect-blues-expert: blues-expert ## Run MCP inspector for blues-expert @: # Documentation targets -docs: docs-notehub docs-notecard docs-blues-expert ## Generate documentation for all packages - -docs-notehub: ## Generate Go documentation for notehub package - @echo "Generating Notehub API documentation..." - @mkdir -p notehub/docs - @go doc -all ./notehub > ./notehub/docs/API_DOCS.md - @echo "✓ Notehub documentation generated: notehub/docs/API_DOCS.md" - -docs-notecard: ## Generate Go documentation for notecard package - @echo "Generating Notecard API documentation..." - @mkdir -p notecard/docs - @go doc -all ./notecard > ./notecard/docs/API_DOCS.md - @echo "✓ Notecard documentation generated: notecard/docs/API_DOCS.md" +docs: docs-blues-expert ## Generate documentation for all packages docs-blues-expert: ## Generate Go documentation for blues-expert package @echo "Generating Blues Expert API documentation..." @@ -81,20 +51,18 @@ test: ## Run tests for all packages clean: ## Clean build artifacts and generated docs @echo "Cleaning build artifacts..." - rm -f $(NOTECARD_DIR)/notecard - rm -f $(NOTEHUB_DIR)/notehub rm -f $(BLUES_EXPERT_DIR)/blues-expert - @rm -rf notehub/docs notecard/docs blues-expert/docs + @rm -rf blues-expert/docs @echo "✓ Clean complete" # Utility targets fmt: ## Format Go code @echo "Formatting Go code..." - @go fmt ./notehub/... ./notecard/... ./blues-expert/... + @go fmt ./blues-expert/... @echo "✓ Code formatted" vet: ## Run go vet on all packages - @go vet ./notehub/... ./notecard/... ./blues-expert/... + @go vet ./blues-expert/... @echo "✓ Vet complete" develop: fmt vet test docs build ## Run full development workflow (format, vet, test, docs, build) diff --git a/README.md b/README.md index 0e80607..20cda3a 100644 --- a/README.md +++ b/README.md @@ -1,51 +1,23 @@ # note-mcp -MCP servers for Notecard, Notehub, and Blues Expert. +Blues Expert MCP server for Notecard & Notehub development. > [!WARNING] -> These MCP servers are experimental and subject to change. Please wait until a versioned release is available before relying on them. +> This MCP server is experimental and subject to change. Please wait until a versioned release is available before relying on it. ## About -This project is a collection of MCP servers for Notecard, Notehub, and Blues Expert. - -### Notecard (STDIO) - -The Notecard MCP server is a local tool design to help you develop Notecard projects. -It provides a simple abstraction to Notecard (using the Notecard CLI), and can help you get started setting up and configuring your Notecards. -This tool provides utilities to update firmware, connect your Notecards to WiFi networks, and more. - -#### Prerequisites - -- [Notecard CLI](https://github.com/blues/notecard-cli) - -### Notehub (STDIO) - -The Notehub MCP server is a local tool design to help you develop Notehub projects. -It provides a simple abstraction to Notehub (using the Notehub API), and can help you get started setting up and configuring your Notehub projects. -This tool provides utilities to create and manage Notehub projects, view Note events,and to manage your Notehub devices. - -### Blues Expert (Docker/HTTP) - -The Blues Expert MCP server is a remote tool design to help you develop Notecard projects. -When used with an LLM, it can help to guide with best practices, for both writing code as well as leveraging Notecard's capabilities. -The Blues Expert MCP provides correct and accurate information about Notecard, and can help to reduce hallucinations and errors generated by LLMs when building Notecard projects. -Use this tool to accelerate your development process, and to ensure that your projects are built correctly and efficiently according to best practices. +The Blues Expert MCP server is a remote tool designed to help you develop Notecard projects. +When used with an LLM, it provides guidance on best practices for writing firmware and leveraging Notecard's capabilities. +It provides correct and accurate information about Notecard, reducing hallucinations and errors when building Notecard projects. ## Build -To build the MCP servers, you'll need to have the following tools installed: +Requirements: -- Go (atleast v1.23) +- Go (at least v1.23) - Make - -Per MCP server, you'll need to have the following tools installed: - -- Notecard MCP: [Notecard CLI](https://github.com/blues/notecard-cli) -- Notehub MCP: Notehub Account with API access -- Blues Expert MCP: [Docker](https://www.docker.com/products/docker-desktop/) - -Then, run the following command: +- [Docker](https://www.docker.com/products/docker-desktop/) ```bash make build @@ -53,58 +25,23 @@ make build ## Install -Add the following to your `mcp.json` file, where `mcp.json` is the file that determines where the MCP servers are located (e.g. for Claude Desktop, this is `claude_desktop_config.json`): +Add the following to your `mcp.json` file (e.g. for Claude Desktop, this is `claude_desktop_config.json`): ```json { "mcpServers": { - "notecard": { - "type": "stdio", - "command": "/YOUR/PATH/TO/note-mcp/notecard/notecard" - }, - "notehub": { - "type": "stdio", - "command": "/YOUR/PATH/TO/note-mcp/notehub/notehub", - "args": [ - "--env", - "/YOUR/PATH/TO/note-mcp/.env" - ] - }, "blues-expert": { "type": "http", "url": "http://localhost:8080/expert/mcp" } - }, + } } ``` -The `.env` file for Notehub MCP should contain the following variables: - -```bash -NOTEHUB_USER="your_notehub_username" -NOTEHUB_PASS="your_notehub_password" -``` - -Additional variables will be added. - ## Development -To run the MCP inspector, you'll need nodejs installed (atleast v18). - -For Notecard MCP: - -```bash -make inspect notecard -``` - -For Notehub MCP: - -```bash -make inspect notehub -``` - -For Blues Expert MCP: +To run the MCP inspector, you'll need Node.js installed (at least v18). ```bash -make inspect blues-expert +make inspect-blues-expert ``` diff --git a/blues-expert/main.go b/blues-expert/main.go index cc590bc..eac9b1e 100644 --- a/blues-expert/main.go +++ b/blues-expert/main.go @@ -7,7 +7,6 @@ import ( "os" "note-mcp/blues-expert/lib" - "note-mcp/utils" "github.com/joho/godotenv" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -71,7 +70,7 @@ func main() { sessionManager = lib.NewSessionManager() // Create a new MCP server - impl := &mcp.Implementation{Name: "Blues Expert MCP", Version: utils.Commit} + impl := &mcp.Implementation{Name: "Blues Expert MCP", Version: commit} opts := &mcp.ServerOptions{ Instructions: "This MCP server provides expert guidance on using the Blues Notecard & Notehub. When using this tool for developing firmware, use the 'firmware_entrypoint' tool to get started. Otherwise, use the 'docs_search' or 'docs_search_expert' tool to search the Blues documentation.", HasTools: true, diff --git a/utils/version.go b/blues-expert/version.go similarity index 82% rename from utils/version.go rename to blues-expert/version.go index a0d68aa..3657739 100644 --- a/utils/version.go +++ b/blues-expert/version.go @@ -1,8 +1,8 @@ -package utils +package main import "runtime/debug" -var Commit = func() string { +var commit = func() string { if info, ok := debug.ReadBuildInfo(); ok { for _, setting := range info.Settings { if setting.Key == "vcs.revision" { diff --git a/go.mod b/go.mod index 7e259fe..893f5ca 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.39.0 github.com/google/jsonschema-go v0.3.0 github.com/joho/godotenv v1.5.1 - github.com/mark3labs/mcp-go v0.38.0 github.com/modelcontextprotocol/go-sdk v1.1.0 github.com/rs/zerolog v1.34.0 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 - golang.org/x/text v0.26.0 ) require ( @@ -27,19 +25,9 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.34.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 // indirect github.com/aws/smithy-go v1.22.5 // indirect - github.com/bahlo/generic-list-go v0.2.0 // indirect - github.com/buger/jsonparser v1.1.1 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/invopop/jsonschema v0.13.0 // indirect - github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect - github.com/spf13/cast v1.7.1 // indirect - github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.22.0 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3c60717..cbc6d35 100644 --- a/go.sum +++ b/go.sum @@ -26,38 +26,14 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.38.0 h1:iV1Ko4Em/lkJIsoKyGfc0nQySi+v github.com/aws/aws-sdk-go-v2/service/sts v1.38.0/go.mod h1:bEPcjW7IbolPfK67G1nilqWyoxYMSPrDiIQ3RdIdKgo= github.com/aws/smithy-go v1.22.5 h1:P9ATCXPMb2mPjYBgueqJNCA5S9UfktsW0tTxi+a7eqw= github.com/aws/smithy-go v1.22.5/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= -github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= -github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= -github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= -github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= -github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= -github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mark3labs/mcp-go v0.38.0 h1:E5tmJiIXkhwlV0pLAwAT0O5ZjUZSISE/2Jxg+6vpq4I= -github.com/mark3labs/mcp-go v0.38.0/go.mod h1:T7tUa2jO6MavG+3P25Oy/jR7iCeJPHImCZHRymCn39g= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= @@ -66,21 +42,11 @@ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/modelcontextprotocol/go-sdk v1.1.0 h1:Qjayg53dnKC4UZ+792W21e4BpwEZBzwgRW6LrjLWSwA= github.com/modelcontextprotocol/go-sdk v1.1.0/go.mod h1:6fM3LCm3yV7pAs8isnKLn07oKtB0MP9LHd3DfAcKw10= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= -github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= @@ -90,12 +56,5 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mcp.json b/mcp.json index d6fe2c0..4230cb1 100644 --- a/mcp.json +++ b/mcp.json @@ -1,22 +1,8 @@ { "mcpServers" : { - "notecard": { - "command": "notecard/notecard", - "args": [ - "--env", - ".env" - ] - }, - "notehub": { - "command": "notehub/notehub", - "args": [ - "--env", - ".env" - ] - }, "blues-expert": { - "type": "http", - "url": "http://localhost:8080/expert/mcp" + "type": "http", + "url": "http://localhost:8080/expert/mcp" } } } diff --git a/notecard/lib/docs.go b/notecard/lib/docs.go deleted file mode 100644 index d657662..0000000 --- a/notecard/lib/docs.go +++ /dev/null @@ -1,216 +0,0 @@ -package lib - -import ( - "fmt" - "io" - "net/http" - "regexp" - "strings" -) - -// API categories for chunked resources -var APICategories = []string{ - "card", "hub", "note", "env", "file", "web", "var", "ntn", "dfu", -} - -// EndpointInfo holds information about an API endpoint -type EndpointInfo struct { - Name string - Category string - Description string -} - -// FetchAPIDocumentation fetches the full API documentation from the Blues website -func FetchAPIDocumentation() (string, error) { - resp, err := http.Get("https://blues.github.io/notecard-schema/index.md") - if err != nil { - return "", fmt.Errorf("failed to fetch API documentation: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return "", fmt.Errorf("failed to fetch API documentation: HTTP %d", resp.StatusCode) - } - - content, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read API documentation: %w", err) - } - - return string(content), nil -} - -// ParseAPIDocumentation splits the full API documentation into category-specific chunks -func ParseAPIDocumentation(content string) map[string]string { - chunks := make(map[string]string) - - // Split content by API endpoints (### `endpoint`) - endpointRegex := regexp.MustCompile(`(?m)^### ` + "`" + `([^` + "`" + `]+)` + "`") - matches := endpointRegex.FindAllStringSubmatchIndex(content, -1) - - // Extract the header (everything before the first endpoint) - var header string - if len(matches) > 0 { - header = content[:matches[0][0]] - } else { - header = content - } - - // Collect all endpoints for the overview - var allEndpoints []EndpointInfo - - // Process each endpoint - for i, match := range matches { - // Extract endpoint name - endpointName := content[match[2]:match[3]] - - // Determine the category (first part before the dot) - category := strings.Split(endpointName, ".")[0] - - // Find the content for this endpoint (from this match to the next one or end) - var endpointContent string - if i < len(matches)-1 { - endpointContent = content[match[0]:matches[i+1][0]] - } else { - endpointContent = content[match[0]:] - } - - // Extract description from the endpoint content - description := extractEndpointDescription(endpointContent) - allEndpoints = append(allEndpoints, EndpointInfo{ - Name: endpointName, - Category: category, - Description: description, - }) - - // Add to the appropriate category chunk - if chunks[category] == "" { - chunks[category] = header + "\n\n" - } - chunks[category] += endpointContent - } - - // Create comprehensive overview with all endpoints - chunks["overview"] = CreateOverview(header, allEndpoints) - - return chunks -} - -// extractEndpointDescription extracts the description from an endpoint's content -func extractEndpointDescription(content string) string { - lines := strings.Split(content, "\n") - - // Look for the first non-empty line after "#### Request" - inRequest := false - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "#### Request" { - inRequest = true - continue - } - if inRequest && line != "" && !strings.HasPrefix(line, "**Parameters:**") && !strings.HasPrefix(line, "|") { - // Clean up the description - description := strings.TrimSpace(line) - // Remove any trailing periods for consistency - description = strings.TrimSuffix(description, ".") - return description - } - } - - return "No description available" -} - -// CreateOverview creates a comprehensive overview with all API endpoints -func CreateOverview(header string, endpoints []EndpointInfo) string { - var overview strings.Builder - - // Add the original header - overview.WriteString(header) - overview.WriteString("\n\n## Available API Endpoints\n\n") - overview.WriteString("The following API endpoints are available, organized by category:\n\n") - - // Group endpoints by category - categoryMap := make(map[string][]EndpointInfo) - for _, endpoint := range endpoints { - categoryMap[endpoint.Category] = append(categoryMap[endpoint.Category], endpoint) - } - - // Define category order and descriptions - categoryDescriptions := map[string]string{ - "card": "Core Notecard functionality and configuration", - "hub": "Notehub connectivity and synchronization", - "note": "Note management and operations", - "env": "Environment variable management", - "file": "File operations and management", - "web": "HTTP/HTTPS web requests", - "var": "Variable storage and retrieval", - "ntn": "NTN (satellite) connectivity", - "dfu": "Device firmware update operations", - } - - // Output endpoints by category in a logical order - categoryOrder := []string{"card", "hub", "note", "env", "file", "web", "var", "ntn", "dfu"} - - for _, category := range categoryOrder { - if endpoints, exists := categoryMap[category]; exists { - overview.WriteString(fmt.Sprintf("### %s APIs\n", strings.Title(category))) - if desc, hasDesc := categoryDescriptions[category]; hasDesc { - overview.WriteString(fmt.Sprintf("*%s*\n\n", desc)) - } - - for _, endpoint := range endpoints { - overview.WriteString(fmt.Sprintf("- **`%s`** - %s\n", endpoint.Name, endpoint.Description)) - } - overview.WriteString("\n") - } - } - - overview.WriteString("## Usage\n\n") - overview.WriteString("To access detailed documentation for any category, use the corresponding resource:\n") - overview.WriteString("- `docs://api/card` - Card API documentation\n") - overview.WriteString("- `docs://api/hub` - Hub API documentation\n") - overview.WriteString("- `docs://api/note` - Note API documentation\n") - overview.WriteString("- `docs://api/env` - Environment API documentation\n") - overview.WriteString("- `docs://api/file` - File API documentation\n") - overview.WriteString("- `docs://api/web` - Web API documentation\n") - overview.WriteString("- `docs://api/var` - Variable API documentation\n") - overview.WriteString("- `docs://api/ntn` - NTN API documentation\n") - overview.WriteString("- `docs://api/dfu` - DFU API documentation\n") - - return overview.String() -} - -// GetAPIOverview fetches and returns the API overview -func GetAPIOverview() (string, error) { - fullContent, err := FetchAPIDocumentation() - if err != nil { - return "", err - } - - chunks := ParseAPIDocumentation(fullContent) - - overview, exists := chunks["overview"] - if !exists { - return "", fmt.Errorf("API overview not found") - } - - return overview, nil -} - -// GetAPICategoryDocumentation fetches and returns documentation for a specific API category -func GetAPICategoryDocumentation(category string) (string, error) { - // Fetch the full API documentation - fullContent, err := FetchAPIDocumentation() - if err != nil { - return "", err - } - - chunks := ParseAPIDocumentation(fullContent) - - content, exists := chunks[category] - if !exists { - return "", fmt.Errorf("API category '%s' not found", category) - } - - return content, nil -} diff --git a/notecard/lib/handlers.go b/notecard/lib/handlers.go deleted file mode 100644 index 9e7d6b7..0000000 --- a/notecard/lib/handlers.go +++ /dev/null @@ -1,732 +0,0 @@ -package lib - -import ( - "context" - "encoding/xml" - "fmt" - "io" - "net/http" - "os" - "os/user" - "regexp" - "strconv" - "strings" - "time" - - "note-mcp/utils" - - "github.com/mark3labs/mcp-go/mcp" -) - -// ConfigDir returns the config directory -func getConfigDir() string { - usr, err := user.Current() - if err != nil { - return "." - } - path := usr.HomeDir + "/note" - return path -} - -// Get the pathname of config settings -func getConfigSettingsPath() string { - return getConfigDir() + "/config.json" -} - -// S3ListBucketResult represents the XML structure returned by S3 list bucket API -type S3ListBucketResult struct { - XMLName xml.Name `xml:"ListBucketResult"` - Contents []struct { - Key string `xml:"Key"` - } `xml:"Contents"` -} - -// compareVersions compares two semantic version strings -// Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if v1 == v2 -func compareVersions(v1, v2 string) int { - parts1 := strings.Split(v1, ".") - parts2 := strings.Split(v2, ".") - - maxLen := len(parts1) - if len(parts2) > maxLen { - maxLen = len(parts2) - } - - for i := 0; i < maxLen; i++ { - var p1, p2 int - if i < len(parts1) { - p1, _ = strconv.Atoi(parts1[i]) - } - if i < len(parts2) { - p2, _ = strconv.Atoi(parts2[i]) - } - - if p1 > p2 { - return 1 - } - if p1 < p2 { - return -1 - } - } - return 0 -} - -// extractKeysFromXml extracts firmware keys from S3 XML response -func extractKeysFromXml(xmlData []byte) ([]string, error) { - var result S3ListBucketResult - if err := xml.Unmarshal(xmlData, &result); err != nil { - return nil, fmt.Errorf("failed to parse XML: %w", err) - } - - keys := make([]string, len(result.Contents)) - for i, content := range result.Contents { - keys[i] = content.Key - } - return keys, nil -} - -// extractAvailableVersions extracts unique version numbers from firmware keys -func extractAvailableVersions(relevantKeys []string) []string { - versionRegex := regexp.MustCompile(`-(\d+\.\d+\.\d+\.\d+)\.(?:bin|dfu)`) - versionSet := make(map[string]bool) - - for _, key := range relevantKeys { - matches := versionRegex.FindStringSubmatch(key) - if len(matches) > 1 { - versionSet[matches[1]] = true - } - } - - versions := make([]string, 0, len(versionSet)) - for version := range versionSet { - versions = append(versions, version) - } - return versions -} - -// getNotecardTypeFromModel determines Notecard type from model string -func getNotecardTypeFromModel(notecardModel string) string { - if notecardModel == "" { - return "" - } - - // Mapping from firmware type prefix to substrings found in model names - notecardTypeMap := map[string][]string{ - "": {"500"}, // e.g., "NOTE-NBGL-500" maps to "" - "u5": {"NB", "MB", "WB"}, // e.g., "NOTE-WBNA", "NOTE-NBGL", "NOTE-MBNA" map to "u5" - "wl": {"LW"}, // e.g., "NOTE-LWL" maps to "wl" - "s3": {"ESP"}, // e.g., "NOTE-ESP32" maps to "s3" - } - - for notecardType, modelSubstrings := range notecardTypeMap { - for _, substring := range modelSubstrings { - if strings.Contains(notecardModel, substring) { - return notecardType - } - } - } - - return "" -} - -// listAvailableFirmwareVersions fetches available firmware versions for a given type -func listAvailableFirmwareVersions(updateChannel, notecardType string) ([]string, error) { - firmwareIndexUrl := fmt.Sprintf("https://s3.us-east-1.amazonaws.com/notecard-firmware?prefix=%s", updateChannel) - - resp, err := http.Get(firmwareIndexUrl) - if err != nil { - return nil, fmt.Errorf("failed to fetch firmware index: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to fetch firmware index: %s", resp.Status) - } - - xmlData, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response body: %w", err) - } - - allKeys, err := extractKeysFromXml(xmlData) - if err != nil { - return nil, fmt.Errorf("failed to extract keys from XML: %w", err) - } - - if len(allKeys) == 0 { - return []string{}, nil - } - - var relevantKeys []string - searchPattern := fmt.Sprintf("-%s-", notecardType) - for _, key := range allKeys { - if strings.Contains(key, searchPattern) { - relevantKeys = append(relevantKeys, key) - } - } - - if len(relevantKeys) == 0 { - return []string{}, nil - } - - // Extract and return unique versions - availableVersions := extractAvailableVersions(relevantKeys) - return availableVersions, nil -} - -// getLatestFirmwareVersion returns the latest firmware version for a given update channel and notecard type -func getLatestFirmwareVersion(updateChannel, notecardType string) (string, error) { - versions, err := listAvailableFirmwareVersions(updateChannel, notecardType) - if err != nil { - return "", err - } - - if len(versions) == 0 { - return "", fmt.Errorf("no firmware versions found for %s/%s", updateChannel, notecardType) - } - - // Find the latest version by comparing all versions - latestVersion := versions[0] - for _, version := range versions[1:] { - if compareVersions(version, latestVersion) > 0 { - latestVersion = version - } - } - - return latestVersion, nil -} - -// downloadFirmware downloads a firmware binary from the given URL -func downloadFirmware(url string) ([]byte, error) { - resp, err := http.Get(url) - if err != nil { - return nil, fmt.Errorf("failed to download firmware: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("failed to download firmware: %s at %s", resp.Status, url) - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read firmware data: %w", err) - } - - return data, nil -} - -// constructFirmwareURL builds the firmware download URL based on parameters -func constructFirmwareURL(updateChannel, notecardType, version string) string { - var filename string - directory := fmt.Sprintf("notecard-%s", version) - - if updateChannel == "DevRel" { - updateChannel = fmt.Sprintf("DevRel/%s", strings.Join(strings.Split(version, ".")[:3], ".")) - } - - if notecardType == "" { - // For NOTE-NBGL-500 and similar - filename = fmt.Sprintf("notecard-%s.bin", version) - } else { - // For other notecard types - filename = fmt.Sprintf("notecard-%s-%s.bin", notecardType, version) - } - - return fmt.Sprintf("https://s3.us-east-1.amazonaws.com/notecard-firmware/%s/%s/%s", updateChannel, directory, filename) -} - -// HandleNotecardInitializeTool handles the notecard initialization -func HandleNotecardInitializeTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - reset := request.GetBool("reset", false) - port := request.GetString("port", "") - baud := request.GetInt("baud", 0) - - if reset { - // check if config exists and delete it - if _, err := os.Stat(getConfigSettingsPath()); err == nil { - os.Remove(getConfigSettingsPath()) - } - } - - if port != "" { - _, err := utils.ExecuteNotecardCommand([]string{"-port", port}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set port: %v", err)), nil - } - } - - if baud != 0 { - _, err := utils.ExecuteNotecardCommand([]string{"-portconfig", strconv.Itoa(baud)}) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set baud rate: %v", err)), nil - } - } - - output, err := utils.ExecuteNotecardCommand([]string{"-req", "{\"req\":\"card.version\"}"}) - - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to initialize notecard: %v", err)), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("Notecard initialized successfully. Output: %s", output)), nil -} - -// HandleNotecardRequestTool handles sending requests to the notecard -func HandleNotecardRequestTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - - reqType, err := request.RequireString("request") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid request parameter: %v", err)), nil - } - - var output string - if _, ok := os.LookupEnv("BLUES"); !ok { - output, err = utils.ExecuteNotecardCommand([]string{"-req", reqType}) - } else { - output, err = utils.ExecuteNotecardCommand([]string{"-req", reqType, "-verbose"}) - } - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to send request to notecard: %v", err)), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("Response:\n%s", output)), nil -} - -// HandleNotecardListFirmwareVersionsTool handles listing firmware versions -func HandleNotecardListFirmwareVersionsTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - updateChannel, err := request.RequireString("updateChannel") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid updateChannel parameter: %v", err)), nil - } - - notecardModel, err := request.RequireString("notecardModel") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid notecardModel parameter: %v", err)), nil - } - - notecardType := getNotecardTypeFromModel(notecardModel) - if notecardType == "" && !strings.Contains(notecardModel, "500") { - return mcp.NewToolResultError(fmt.Sprintf("Could not determine Notecard type for model '%s'. Check the provided model.", notecardModel)), nil - } - - versions, err := listAvailableFirmwareVersions(updateChannel, notecardType) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Could not list firmware versions: %v", err)), nil - } - - if len(versions) == 0 { - return mcp.NewToolResultText(fmt.Sprintf("No firmware versions found for %s/%s", updateChannel, notecardModel)), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("Available firmware versions for %s: %s", updateChannel, strings.Join(versions, ", "))), nil -} - -// HandleNotecardUpdateFirmwareTool handles firmware updates -func HandleNotecardUpdateFirmwareTool(logger *utils.MCPLogger) func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - return func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - updateChannel := request.GetString("updateChannel", "LTS") - notecardModel, err := request.RequireString("notecardModel") - if err != nil { - logger.Errorf("Firmware update failed: Invalid notecardModel parameter: %v", err) - return mcp.NewToolResultError(fmt.Sprintf("Invalid notecardModel parameter: %v", err)), nil - } - - version := request.GetString("version", "") - force := request.GetBool("force", false) - - // Create a unique progress token for this operation - progressToken := fmt.Sprintf("firmware_update_%d", time.Now().Unix()) - - // Helper function to send MCP-compliant progress notifications - sendProgress := func(progress, total float64, message string) { - logger.SendProgressNotification(progressToken, progress, total, message) - } - - // Log the start of firmware update with context - updateContext := map[string]interface{}{ - "updateChannel": updateChannel, - "notecardModel": notecardModel, - "version": version, - "force": force, - } - logger.LogWithContext(utils.LogLevelInfo, "Starting firmware update process", updateContext) - sendProgress(0, 100, "Starting firmware update process...") - - // Determine notecard type from model - notecardType := getNotecardTypeFromModel(notecardModel) - if notecardType == "" && !strings.Contains(notecardModel, "500") { - logger.Errorf("Could not determine Notecard type for model '%s'", notecardModel) - return mcp.NewToolResultError(fmt.Sprintf("Could not determine Notecard type for model '%s'. Check the provided model.", notecardModel)), nil - } - - logger.Infof("Determined Notecard type: '%s' for model: '%s'", notecardType, notecardModel) - sendProgress(5, 100, fmt.Sprintf("Determined Notecard type: %s", notecardType)) - - // If no version specified, get the latest version - if version == "" { - logger.Info("No version specified, fetching latest version...") - sendProgress(10, 100, "Fetching latest firmware version...") - - latestVersion, err := getLatestFirmwareVersion(updateChannel, notecardType) - if err != nil { - logger.Errorf("Failed to get latest firmware version: %v", err) - return mcp.NewToolResultError(fmt.Sprintf("Failed to get latest firmware version: %v", err)), nil - } - version = latestVersion - logger.Infof("Using latest version: %s", version) - sendProgress(15, 100, fmt.Sprintf("Using latest version: %s", version)) - } - - // Get current firmware version for comparison - if !force { - logger.Info("Checking current firmware version...") - sendProgress(20, 100, "Checking current firmware version...") - - response, err := utils.ExecuteNotecardCommand([]string{"-req", "{\"req\":\"card.version\"}"}) - if err != nil { - logger.Errorf("Failed to get current firmware version: %v", err) - return mcp.NewToolResultError(fmt.Sprintf("Failed to get current firmware version: %v", err)), nil - } - - if strings.Contains(response, version) { - logger.Warningf("Notecard is already running version %s, skipping update", version) - sendProgress(100, 100, fmt.Sprintf("Already running version %s", version)) - return mcp.NewToolResultText(fmt.Sprintf("Notecard is already running version %s. Use force=true to update anyway.", version)), nil - } - } else { - logger.Info("Force update enabled, skipping version check") - sendProgress(20, 100, "Force update enabled, skipping version check") - } - - firmwareURL := constructFirmwareURL(updateChannel, notecardType, version) - logger.Infof("Firmware download URL: %s", firmwareURL) - sendProgress(25, 100, "Preparing firmware download...") - - logger.Info("Starting firmware download...") - sendProgress(30, 100, "Downloading firmware...") - firmwareData, err := downloadFirmware(firmwareURL) - if err != nil { - logger.Errorf("Failed to download firmware: %v", err) - return mcp.NewToolResultError(fmt.Sprintf("Failed to download firmware: %v", err)), nil - } - - // write firmwareData to a file - err = os.WriteFile("/tmp/firmware.bin", firmwareData, 0644) - if err != nil { - logger.Errorf("Failed to write firmware to file: %v", err) - return mcp.NewToolResultError(fmt.Sprintf("Failed to write firmware to file: %v", err)), nil - } - - logger.Infof("Successfully downloaded firmware: %d bytes", len(firmwareData)) - sendProgress(40, 100, fmt.Sprintf("Downloaded firmware: %d bytes", len(firmwareData))) - - logger.Info("Starting firmware sideload to Notecard...") - sendProgress(45, 100, "Starting firmware sideload...") - - _, err = utils.ExecuteNotecardCommandWithLogger([]string{"-sideload", "/tmp/firmware.bin", "-fast"}, logger) - if err != nil { - logger.Errorf("Failed to sideload firmware: %v", err) - return mcp.NewToolResultError(fmt.Sprintf("Failed to sideload firmware: %v", err)), nil - } - - logger.Info("Firmware sideload completed successfully") - sendProgress(100, 100, "Firmware sideload completed.") - - response, err := utils.ExecuteNotecardCommand([]string{"-req", "{\"req\":\"card.version\"}"}) - if err != nil { - logger.Warningf("Could not verify new firmware version: %v", err) - return mcp.NewToolResultText(fmt.Sprintf("Firmware update completed to version %s, but could not verify the new version. The Notecard connection has been reestablished.", version)), nil - } - - var newVersion string - if strings.Contains(response, version) { - newVersion = version - } - - if newVersion != "" { - logger.Infof("Firmware update completed successfully. New version: %s", newVersion) - - // Log completion with context - completionContext := map[string]interface{}{ - "previousVersion": "unknown", // We could store this from earlier - "newVersion": newVersion, - "updateChannel": updateChannel, - "notecardModel": notecardModel, - } - logger.LogWithContext(utils.LogLevelInfo, "Firmware update completed successfully", completionContext) - - return mcp.NewToolResultText(fmt.Sprintf("Successfully updated Notecard firmware to %s %s. Connection reestablished and verified.", updateChannel, newVersion)), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("Successfully updated Notecard firmware to version %s. The Notecard has restarted and connection has been reestablished.", version)), nil - } -} - -// HandleNotecardValidateRequestTool handles request validation -func HandleNotecardValidateRequestTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - reqString, err := request.RequireString("request") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid request parameter: %v", err)), nil - } - - schemaURL := request.GetString("schema_url", "") - // set the NOTE_JSON_SCHEMA_URL environment variable if a schema URL is provided, otherwise unset it - if schemaURL != "" { - os.Setenv("NOTE_JSON_SCHEMA_URL", schemaURL) - } else { - os.Unsetenv("NOTE_JSON_SCHEMA_URL") - } - - output, err := utils.ExecuteNotecardCommandWithEnv([]string{"-req", reqString, "-dry", "-verbose"}, os.Environ()) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Attempt to validate request failed: %v", err)), nil - } - - output = strings.ReplaceAll(output, "\n", "") - - // If the output is not equal to the request, return the reason why it failed - if output != reqString { - return mcp.NewToolResultText(fmt.Sprintf("Validation for %s failed: %s", reqString, output)), nil - } - - // if the BLUES environment variable is not set, return a warning - if _, ok := os.LookupEnv("BLUES"); !ok { - return mcp.NewToolResultText(fmt.Sprintf("✓ Request validation successful: The request '%s' is valid JSON.", reqString)), nil - } else { - return mcp.NewToolResultText(fmt.Sprintf("✓ Request validation successful: The request '%s' is valid according to the Notecard API schema.", reqString)), nil - } -} - -// HandleDecryptNoteTool handles the decrypt note tool -func HandleDecryptNoteTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - noteFile := request.GetString("note_file", "mysecrets.qi") - - instructions := fmt.Sprintf(`To decrypt the note file '%s', follow these steps: - -1. First, sync with the hub to get any pending notefiles: - {"req":"hub.sync"} - -2. Then decrypt and retrieve the note: - {"req":"note.get","file":"%s","decrypt":true,"delete":true} - -Key points: -- The "decrypt":true flag tells the Notecard to decrypt the note contents -- The "delete":true flag removes the note from the queue after retrieval -- Make sure the Notecard has the proper encryption keys configured -- If decryption fails, check that the note was encrypted with the correct public key`, noteFile, noteFile) - - return mcp.NewToolResultText(instructions), nil -} - -// HandleProvisionNotecardTool handles the provision notecard tool -func HandleProvisionNotecardTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - productUID, err := request.RequireString("product_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid product_uid parameter: %v", err)), nil - } - - _, err = request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - ssid := request.GetString("ssid", "") - password := request.GetString("password", "") - var wifiInstructions string - if ssid != "" && password != "" { - wifiInstructions = fmt.Sprintf(` - 2a. Configure the Notecard to connect to your WiFi network: {"req":"card.wifi","ssid":"%s","password":"%s"}`, ssid, password) - } else { - wifiInstructions = "" - } - - instructions := fmt.Sprintf(`To provision your Notecard for the Notehub with product '%s', follow these steps (IMPORTANT: Use the 'notecard_request' tool to send the following requests to the Notecard): - -1. Restore the Notecard to factory defaults: - {"req":"card.restore","delete":true,"connected":true} - -2. Configure the Notecard to connect to your project: - {"req":"hub.set","product":"%s","mode":"continuous"} -%s -3. Sync with the hub to complete provisioning: - {"req":"hub.sync"} - -4. The Notecard should now appear in your Notehub dashboard. You can verify successful connection to Notehub by checking the {"req":"hub.status"} response for "connected":true. - -5. You should follow up by using the 'project_events' in the Notehub MCP tool to verify that the Notecard has uploaded the Note file to Notehub. This can sometimes take a short while to propagate, check again after a few seconds if no events are shown. - -6. Provide a link to the 'project_url', to help the user verify the provisioning. - -Important Notes: -- Ensure you have the correct Product UID from your Notehub project -- For WiFi Notecards, WiFi credentials are required for successful provisioning -- The Notecard will restart during the provisioning process -- Provisioning may take a few minutes to complete fully`, productUID, productUID, wifiInstructions) - - return mcp.NewToolResultText(instructions), nil -} - -// HandleTroubleshootConnectionTool handles the troubleshoot connection tool -func HandleTroubleshootConnectionTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - errorMessage, err := request.RequireString("error_message") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid error_message parameter: %v", err)), nil - } - - notecardType, err := request.RequireString("notecard_type") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid notecard_type parameter: %v", err)), nil - } - - var typeSpecificSteps string - if notecardType == "WiFi" { - typeSpecificSteps = ` -MODEL-SPECIFIC STEPS (WiFi Notecard): -- Verify WiFi credentials: {"req":"card.wifi"} -- Check WiFi signal strength in the response -- Try connecting to a different WiFi network if signal is weak -` - } else if notecardType == "Cellular" { - typeSpecificSteps = ` -MODEL-SPECIFIC STEPS (Cellular Notecard): -- Check SIM card status: {"req":"card.wireless"} -- Verify cellular signal strength (look for "rssi" and "bars" in response) -- Ensure SIM card is properly inserted and activated -- Try moving to a location with better cellular coverage -` - } - - instructions := fmt.Sprintf(`TROUBLESHOOTING NOTECARD CONNECTION TO NOTEHUB - -Error context: %s - -Follow these diagnostic steps in order: - -1. CHECK CURRENT STATUS: - {"req":"hub.status"} - - Look for "connected":true in the response - - Note any error messages in the "status" field - -2. VERIFY BASIC CONNECTIVITY: - {"req":"card.status"} - - Check if the Notecard is responsive - - Verify basic health indicators - -3. CHECK PRODUCT CONFIGURATION: - {"req":"hub.get"} - - Verify the product UID is correct - - Ensure mode is set appropriately (usually "continuous") - -4. NETWORK CONNECTIVITY CHECK: -%s -5. VERIFY TIME SYNCHRONIZATION: - {"req":"card.time"} - - Ensure the Notecard has correct time (required for secure connections) - -6. FORCE SYNC ATTEMPT: - {"req":"hub.sync"} - - Manually trigger a sync and observe any error messages - -7. CHECK FOR PENDING CONFIGURATION: - {"req":"hub.sync","allow":true} - - This allows downloading any pending configuration from Notehub - -8. RESET AND REPROVISION (if above steps fail): - - Factory reset: {"req":"card.restore","delete":true,"connected":true} - - Reconfigure: {"req":"hub.set","product":"YOUR_PRODUCT_UID","mode":"continuous"} - - For WiFi: {"req":"card.wifi","ssid":"YOUR_SSID","password":"YOUR_PASSWORD"} - - Sync: {"req":"hub.sync"} - -9. FIRMWARE UPDATE (last resort): - - Check current version: {"req":"card.version"} - - Consider updating to latest firmware if version is outdated - -COMMON ISSUES: -- Incorrect product UID -- WiFi credentials wrong or network blocked -- Poor signal strength -- Outdated firmware -- Time synchronization issues - -If issues persist, check the Notehub dashboard for device status and any error logs.`, errorMessage, typeSpecificSteps) - - return mcp.NewToolResultText(instructions), nil -} - -// HandleSendNoteTool handles the send note tool -func HandleSendNoteTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - noteFile := request.GetString("note_file", "data.qo") - noteData := request.GetString("note_data", "") - - var dataExample string - if noteData != "" { - dataExample = fmt.Sprintf(`{"req":"note.add","file":"%s","body":%s}`, noteFile, noteData) - } else { - dataExample = fmt.Sprintf(`{"req":"note.add","file":"%s","body":{"temperature":23.5,"humidity":65,"timestamp":"2024-01-15T10:30:00Z"}}`, noteFile) - } - - instructions := fmt.Sprintf(`To send notes from your Notecard to Notehub, follow these steps: - -1. ADD A NOTE TO THE NOTEFILE: - %s - - Key points about the note.add request: - - "file": The notefile name (use .qo for outbound queues, .dbo for databases) - - "body": JSON object containing your data - - Optional "sync": Set to true to immediately sync this note - -2. SYNC THE NOTE TO NOTEHUB: - {"req":"hub.sync"} - - This uploads all pending notes to Notehub. The Notecard will automatically sync based on your sync settings, but you can force an immediate sync. - -3. VERIFY THE SYNC STATUS: - {"req":"hub.status"} - - Check the response for: - - "connected": true (confirms connection to Notehub) - - "count": Shows number of notes pending sync - - "completed": Shows total completed syncs - -4. CHECK FOR SYNC ERRORS: - {"req":"hub.log"} - - This shows recent sync activity and any error messages. - -NOTEFILE NAMING CONVENTIONS: -- .qo: Outbound queue (one-way from device to cloud) -- .qi: Inbound queue (one-way from cloud to device) -- .dbo: Outbound database (bidirectional sync, device to cloud) -- .dbi: Inbound database (bidirectional sync, cloud to device) -- .db: Bidirectional database (both directions) - -TIPS: -- Use descriptive filenames like "sensors.qo" or "alerts.qo" -- Keep note bodies reasonably small (under 8KB recommended) -- The Notecard will automatically batch multiple small notes for efficient transmission -- Use "sync":true in note.add for time-critical data that needs immediate delivery`, dataExample) - - return mcp.NewToolResultText(instructions), nil -} - -// HandleNotecardGetAPIsTool handles the get APIs tool -func HandleNotecardGetAPIsTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - category := request.GetString("category", "") - - if category == "" { - // Return overview of all APIs - overview, err := GetAPIOverview() - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to fetch API overview: %v", err)), nil - } - return mcp.NewToolResultText(overview), nil - } else { - // Return specific category documentation - categoryDoc, err := GetAPICategoryDocumentation(category) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to fetch API category documentation: %v", err)), nil - } - return mcp.NewToolResultText(categoryDoc), nil - } -} diff --git a/notecard/main.go b/notecard/main.go deleted file mode 100644 index 933fbc5..0000000 --- a/notecard/main.go +++ /dev/null @@ -1,92 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - "os" - - "note-mcp/notecard/lib" - "note-mcp/utils" - - "github.com/joho/godotenv" - "github.com/mark3labs/mcp-go/server" -) - -var ( - envFilePath string -) - -func init() { - flag.StringVar(&envFilePath, "env", "", "Path to .env file to load environment variables") -} - -func main() { - flag.Parse() - - // Load environment variables from .env file if specified - if envFilePath != "" { - err := godotenv.Load(envFilePath) - if err != nil { - log.Printf("Warning: Failed to load .env file '%s': %v", envFilePath, err) - } - } - - // Create a new MCP server - s := server.NewMCPServer( - "Notecard MCP", - utils.Commit, - server.WithToolCapabilities(true), - server.WithResourceCapabilities(true, true), - server.WithRecovery(), - ) - - // Create MCP logger - logger := utils.NewMCPLogger(s, "notecard-mcp") - - // Send initial startup log - logger.Info("Notecard MCP server starting up...") - - // Create resources using functions from resources.go - APIResources := CreateAPIResources() - - // Add tools - notecardInitializeTool := CreateNotecardInitializeTool() - notecardRequestTool := CreateNotecardRequestTool() - notecardListFirmwareVersionsTool := CreateNotecardListFirmwareVersionsTool() - notecardUpdateFirmwareTool := CreateNotecardUpdateFirmwareTool() - decryptNoteTool := CreateDecryptNoteTool() - provisionNotecardTool := CreateProvisionNotecardTool() - troubleshootConnectionTool := CreateTroubleshootConnectionTool() - sendNoteTool := CreateSendNoteTool() - notecardGetAPIsTool := CreateNotecardGetAPIsTool() - - // Add Docs API resources with their handlers - for _, resource := range APIResources { - s.AddResource(resource, HandleAPIResource) - } - - // Add tool handlers - s.AddTool(notecardInitializeTool, lib.HandleNotecardInitializeTool) - s.AddTool(notecardRequestTool, lib.HandleNotecardRequestTool) - s.AddTool(notecardListFirmwareVersionsTool, lib.HandleNotecardListFirmwareVersionsTool) - s.AddTool(notecardUpdateFirmwareTool, lib.HandleNotecardUpdateFirmwareTool(logger)) - s.AddTool(decryptNoteTool, lib.HandleDecryptNoteTool) - s.AddTool(provisionNotecardTool, lib.HandleProvisionNotecardTool) - s.AddTool(troubleshootConnectionTool, lib.HandleTroubleshootConnectionTool) - s.AddTool(sendNoteTool, lib.HandleSendNoteTool) - s.AddTool(notecardGetAPIsTool, lib.HandleNotecardGetAPIsTool) - - // Add tools hidden behind BLUES environment variable - if _, ok := os.LookupEnv("BLUES"); ok { - notecardValidateRequestTool := CreateNotecardValidateRequestTool() - s.AddTool(notecardValidateRequestTool, lib.HandleNotecardValidateRequestTool) - } - - logger.Info("Notecard MCP server ready with logging capabilities") - - if err := server.ServeStdio(s); err != nil { - logger.Errorf("Server error: %v", err) - fmt.Printf("Server error: %v\n", err) - } -} diff --git a/notecard/resources.go b/notecard/resources.go deleted file mode 100644 index 326aa7c..0000000 --- a/notecard/resources.go +++ /dev/null @@ -1,72 +0,0 @@ -package main - -import ( - "context" - "fmt" - "strings" - - "note-mcp/notecard/lib" - - "github.com/mark3labs/mcp-go/mcp" - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -// titleCaser is used to convert strings to title case -var titleCaser = cases.Title(language.English) - -// CreateAPIResources creates multiple Notecard API documentation resources, one for each category -func CreateAPIResources() []mcp.Resource { - var resources []mcp.Resource - - // Create a resource for each API category - for _, category := range lib.APICategories { - resources = append(resources, mcp.NewResource( - fmt.Sprintf("docs://api/%s", category), - fmt.Sprintf("Notecard %s API", titleCaser.String(category)), - mcp.WithResourceDescription(fmt.Sprintf("The Notecard %s API Documentation", titleCaser.String(category))), - mcp.WithMIMEType("text/markdown"), - )) - } - - // Also create a general overview resource - resources = append(resources, mcp.NewResource( - "docs://api/overview", - "Notecard API Overview", - mcp.WithResourceDescription("Overview of the Notecard API with general information"), - mcp.WithMIMEType("text/markdown"), - )) - - return resources -} - -// HandleAPIResource handles requests for category-specific API documentation resources -func HandleAPIResource(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) { - // Extract category from URI (docs://api/{category}) - uriParts := strings.Split(request.Params.URI, "/") - if len(uriParts) < 3 { - return nil, fmt.Errorf("invalid resource URI: %s", request.Params.URI) - } - category := uriParts[len(uriParts)-1] - - var content string - var err error - - if category == "overview" { - content, err = lib.GetAPIOverview() - } else { - content, err = lib.GetAPICategoryDocumentation(category) - } - - if err != nil { - return nil, err - } - - return []mcp.ResourceContents{ - mcp.TextResourceContents{ - URI: request.Params.URI, - MIMEType: "text/markdown", - Text: content, - }, - }, nil -} diff --git a/notecard/tools.go b/notecard/tools.go deleted file mode 100644 index a8b10ce..0000000 --- a/notecard/tools.go +++ /dev/null @@ -1,143 +0,0 @@ -package main - -import ( - "github.com/mark3labs/mcp-go/mcp" -) - -func CreateNotecardInitializeTool() mcp.Tool { - return mcp.NewTool("notecard_initialize", - mcp.WithDescription("Initialize a connection to the Notecard for communication. This creates a notecard object that can be used for subsequent operations."), - mcp.WithString("port", - mcp.Description("The port to connect to the Notecard. If not provided, the default port will be used."), - ), - mcp.WithNumber("baud", - mcp.Description("The baud rate to connect to the Notecard. If not provided, the default baud rate will be used."), - ), - mcp.WithBoolean("reset", - mcp.Description("Reset the Notecard connection and initialize a new connection. Only use this if instructed to do so by the user as it will delete the config file. If provided, the default connection will be used."), - ), - ) -} - -func CreateNotecardRequestTool() mcp.Tool { - return mcp.NewTool("notecard_request", - mcp.WithDescription("Send a request to the Notecard and return the response. The notecard must be initialized first and you should check you have up-to-date API documentation before sending it (by using the 'notecard_get_apis' tool). The request type is the JSON string of the request to send to the Notecard, e.g. '{\"req\":\"card.version\"}' or '{\"req\":\"card.temp\",\"minutes\":60}'. If a request is unknown, you can use the 'notecard_request_validate' tool to validate it. If the request is valid, you can use this tool to send it to the Notecard."), - mcp.WithString("request", - mcp.Required(), - mcp.Description("The request type to send to the Notecard (e.g., '{\"req\":\"card.version\"}', '{\"req\":\"card.status\"}', '{\"req\":\"hub.status\"}', '{\"req\":\"card.temp\",\"minutes\":60}', '{\"req\":\"card.voltage\"}')"), - ), - ) -} - -func CreateNotecardListFirmwareVersionsTool() mcp.Tool { - return mcp.NewTool("notecard_firmware_versions", - mcp.WithDescription("Lists all available firmware versions for a given update channel and Notecard model."), - mcp.WithString("updateChannel", - mcp.Required(), - mcp.Description("The type of update to list versions for (e.g., 'LTS', 'DevRel', 'nightly')"), - ), - mcp.WithString("notecardModel", - mcp.Required(), - mcp.Description("The model of Notecard to list versions for (e.g., 'NOTE-WBEXW', 'NOTE-NBGL-500', etc.)"), - ), - ) -} - -func CreateNotecardUpdateFirmwareTool() mcp.Tool { - return mcp.NewTool("notecard_firmware_update", - mcp.WithDescription("Downloads and flashes firmware to the Notecard. This tool will download the specified firmware version and perform a sideload update to the connected Notecard. Warn the user that this may take some time and that the Notecard will restart. The Notecard must be initialized first."), - mcp.WithString("updateChannel", - mcp.Description("The firmware update channel (e.g., 'LTS', 'DevRel', 'nightly')"), - ), - mcp.WithString("notecardModel", - mcp.Required(), - mcp.Description("The model of Notecard to update (e.g., 'NOTE-WBEXW', 'NOTE-NBGL-500', etc.)"), - ), - mcp.WithString("version", - mcp.Description("The specific firmware version to install (e.g., '8.1.4.17149$20250319220838'). If not provided, will use the latest available version."), - ), - mcp.WithBoolean("force", - mcp.Description("Force the update even if the version is the same or older (default: false)"), - ), - ) -} - -func CreateNotecardValidateRequestTool() mcp.Tool { - return mcp.NewTool("notecard_request_validate", - mcp.WithDescription("Validate a Notecard API request against the Notecard API Schema. This helps ensure your request is valid before sending it to the Notecard. To use this tool, you must have the environment variable BLUES exported, otherwise it will just check for JSON validity."), - mcp.WithString("request", - mcp.Required(), - mcp.Description("The JSON string of the request to validate (e.g., '{\"req\":\"card.version\"}', '{\"req\":\"card.temp\",\"minutes\":60}')"), - ), - mcp.WithString("schema_url", - mcp.Description("The schema URL to validate against. If not provided, uses the default Notecard API schema."), - ), - ) -} - -func CreateDecryptNoteTool() mcp.Tool { - return mcp.NewTool("notecard_decrypt_note", - mcp.WithDescription("Get instructions for decrypting a note using the Notecard. Returns step-by-step instructions to sync with the hub and decrypt the note."), - mcp.WithString("note_file", - mcp.Description("The note file to decrypt (e.g., 'mysecrets.qi'). Defaults to 'mysecrets.qi' if not provided."), - ), - ) -} - -func CreateProvisionNotecardTool() mcp.Tool { - return mcp.NewTool("notecard_provision", - mcp.WithDescription("Get instructions for provisioning a new Notecard to a Notehub project, creating a new project if necessary. Returns comprehensive provisioning steps including WiFi configuration if applicable."), - mcp.WithString("product_uid", - mcp.Required(), - mcp.Description("Product UID within the project, typically in the format 'com.company.username:productname'. Do not assume you know the Product UID. You should first find the Product UID using the 'product_list' tool via the Notehub MCP tool."), - ), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("Project UID, typically in the format 'app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'."), - ), - mcp.WithString("ssid", - mcp.Description("Optional SSID for WiFi provisioning. Do not assume you know the SSID. If the Notecard supports WiFi, stop and ask the user for the SSID."), - ), - mcp.WithString("password", - mcp.Description("Optional password for WiFi provisioning. Do not assume you know the password. If the Notecard supports WiFi, stop and ask the user for the password."), - ), - ) -} - -func CreateTroubleshootConnectionTool() mcp.Tool { - return mcp.NewTool("notecard_troubleshoot_connection", - mcp.WithDescription("Get troubleshooting instructions for when a Notecard will not connect to Notehub. Provides step-by-step diagnostic and resolution suggestions."), - mcp.WithString("error_message", - mcp.Required(), - mcp.Description("Error message or symptom description from the Notecard"), - ), - mcp.WithString("notecard_type", - mcp.Required(), - mcp.Description("Notecard type (e.g., 'WiFi', 'Cellular', 'LoRa') to provide type-specific troubleshooting"), - ), - ) -} - -func CreateSendNoteTool() mcp.Tool { - return mcp.NewTool("notecard_send_note", - mcp.WithDescription("Get instructions for sending notes from a Notecard to Notehub. Returns detailed steps for adding data to notefiles and syncing to the cloud."), - mcp.WithString("note_file", - mcp.Description("The name of the notefile to send data to (e.g., 'sensors.qo', 'data.qo'). Use '.qo' extension for outbound queue files or '.dbo' for database files. To use TLS encryption, use .qos and .dbos. Defaults to 'data.qo' if not provided."), - ), - mcp.WithString("note_data", - mcp.Description("JSON string of the data to include in the note body, e.g. '{\"temperature\":23.5,\"humidity\":65,\"timestamp\":\"2024-01-15T10:30:00Z\"}'"), - ), - mcp.WithString("note_sync", - mcp.Description("Sync the note immediately. Defaults to false."), - ), - ) -} - -func CreateNotecardGetAPIsTool() mcp.Tool { - return mcp.NewTool("notecard_get_apis", - mcp.WithDescription("Get comprehensive documentation for all Notecard APIs or a specific API category. Returns detailed API information organized by category including endpoints, parameters, and usage examples."), - mcp.WithString("category", - mcp.Description("Optional API category to get specific documentation for. Valid categories: 'card', 'hub', 'note', 'env', 'file', 'web', 'var', 'ntn', 'dfu'. If not provided, returns overview of all APIs."), - ), - ) -} diff --git a/notehub/lib/auth.go b/notehub/lib/auth.go deleted file mode 100644 index 0253059..0000000 --- a/notehub/lib/auth.go +++ /dev/null @@ -1,91 +0,0 @@ -package lib - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - - "github.com/joho/godotenv" -) - -// SessionToken holds the current session token for API requests -var SessionToken string - -// NotehubCredentials holds the username and password for Notehub authentication -type NotehubCredentials struct { - Username string - Password string -} - -// LoginRequest represents the login request payload for Notehub authentication -type LoginRequest struct { - Username string `json:"username"` - Password string `json:"password"` -} - -// LoginResponse represents the response from Notehub login containing the session token -type LoginResponse struct { - SessionToken string `json:"session_token"` -} - -// GetNotehubCredentials loads credentials from .env file -func GetNotehubCredentials(envFilePath string) (NotehubCredentials, error) { - envFile, err := godotenv.Read(envFilePath) - if err != nil { - return NotehubCredentials{}, fmt.Errorf("failed to read .env file: %w", err) - } - - envFileUsername := envFile["NOTEHUB_USER"] - envFilePassword := envFile["NOTEHUB_PASS"] - - if envFileUsername == "" { - return NotehubCredentials{}, fmt.Errorf("NOTEHUB_USER not found in .env file") - } - - if envFilePassword == "" { - return NotehubCredentials{}, fmt.Errorf("NOTEHUB_PASS not found in .env file") - } - - return NotehubCredentials{ - Username: envFileUsername, - Password: envFilePassword, - }, nil -} - -// CreateSessionToken creates a session token using username and password -func CreateSessionToken(username, password string) (string, error) { - loginReq := LoginRequest{ - Username: username, - Password: password, - } - - jsonData, err := json.Marshal(loginReq) - if err != nil { - return "", fmt.Errorf("failed to marshal login request: %w", err) - } - - resp, err := http.Post("https://api.notefile.net/auth/login", "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return "", fmt.Errorf("failed to make login request: %w", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("login failed with status %d: %s", resp.StatusCode, string(body)) - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response body: %w", err) - } - - var loginResp LoginResponse - if err := json.Unmarshal(body, &loginResp); err != nil { - return "", fmt.Errorf("failed to unmarshal response: %w", err) - } - - return loginResp.SessionToken, nil -} diff --git a/notehub/lib/client.go b/notehub/lib/client.go deleted file mode 100644 index e87a9ba..0000000 --- a/notehub/lib/client.go +++ /dev/null @@ -1,48 +0,0 @@ -package lib - -import ( - "bytes" - "fmt" - "io" - "net/http" -) - -// MakeNotehubAPIRequest makes an authenticated request to the Notehub API -func MakeNotehubAPIRequest(method, endpoint string, body []byte) (string, error) { - baseURL := "https://api.notefile.net" - url := baseURL + endpoint - - var req *http.Request - var err error - - if body != nil { - req, err = http.NewRequest(method, url, bytes.NewBuffer(body)) - } else { - req, err = http.NewRequest(method, url, nil) - } - - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("X-SESSION-TOKEN", SessionToken) - req.Header.Set("Content-Type", "application/json") - - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return "", fmt.Errorf("failed to make request: %w", err) - } - defer resp.Body.Close() - - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return "", fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return fmt.Sprintf("Request failed with status %d: %s", resp.StatusCode, string(responseBody)), nil - } - - return string(responseBody), nil -} diff --git a/notehub/lib/crypto.go b/notehub/lib/crypto.go deleted file mode 100644 index ce1397d..0000000 --- a/notehub/lib/crypto.go +++ /dev/null @@ -1,100 +0,0 @@ -package lib - -import ( - "bytes" - "crypto/aes" - "crypto/cipher" - "crypto/ecdh" - "crypto/ecdsa" - "crypto/rand" - "crypto/sha256" - "crypto/x509" - "encoding/base64" - "encoding/pem" - "fmt" -) - -// EncryptedNote represents the encrypted note data in Blues format -type EncryptedNote struct { - Alg string `json:"alg"` - Data string `json:"data"` - Key string `json:"key"` -} - -// EncryptMessage encrypts a message using ECDH key exchange and AES-256-CBC encryption -func EncryptMessage(publicKeyPEM string, message []byte) (*EncryptedNote, error) { - // Parse the PEM-encoded public key - pemBlock, _ := pem.Decode([]byte(publicKeyPEM)) - if pemBlock == nil { - return nil, fmt.Errorf("failed to decode PEM block") - } - - // Parse the public key - publicKeyInterface, err := x509.ParsePKIXPublicKey(pemBlock.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse public key: %w", err) - } - - // Convert to ECDSA public key - ecdsaPublicKey, ok := publicKeyInterface.(*ecdsa.PublicKey) - if !ok { - return nil, fmt.Errorf("public key is not an ECDSA key") - } - - // Convert to ECDH public key (P-384 curve) - curve := ecdh.P384() - - // Generate ephemeral key pair - ephemeralPrivateKey, err := curve.GenerateKey(rand.Reader) - if err != nil { - return nil, fmt.Errorf("failed to generate ephemeral key: %w", err) - } - ephemeralPublicKey := ephemeralPrivateKey.PublicKey() - - // Convert the ECDSA public key to ECDH format - pubKeyBytes := append([]byte{0x04}, append(ecdsaPublicKey.X.Bytes(), ecdsaPublicKey.Y.Bytes()...)...) - recipientPublicKey, err := curve.NewPublicKey(pubKeyBytes) - if err != nil { - return nil, fmt.Errorf("failed to create ECDH public key: %w", err) - } - - // Perform ECDH key exchange - sharedSecret, err := ephemeralPrivateKey.ECDH(recipientPublicKey) - if err != nil { - return nil, fmt.Errorf("failed to perform ECDH: %w", err) - } - - // Derive AES key from shared secret using SHA256 - hash := sha256.Sum256(sharedSecret) - aesKey := hash[:] // Use full 32-byte hash for AES-256 - - // Use zero IV - iv := make([]byte, aes.BlockSize) - - // Apply PKCS#7 padding - paddingLength := aes.BlockSize - (len(message) % aes.BlockSize) - padding := bytes.Repeat([]byte{byte(paddingLength)}, paddingLength) - paddedMessage := append(message, padding...) - - // Encrypt with AES-256-CBC - cipherBlock, err := aes.NewCipher(aesKey) - if err != nil { - return nil, fmt.Errorf("failed to create cipher: %w", err) - } - - ciphertext := make([]byte, len(paddedMessage)) - mode := cipher.NewCBCEncrypter(cipherBlock, iv) - mode.CryptBlocks(ciphertext, paddedMessage) - - // Convert ephemeral public key to DER format and base64 encode - ephemeralKeyDER, err := x509.MarshalPKIXPublicKey(ephemeralPublicKey) - if err != nil { - return nil, fmt.Errorf("failed to marshal ephemeral public key: %w", err) - } - - return &EncryptedNote{ - Alg: "secp384r1-aes256cbc", - Data: base64.StdEncoding.EncodeToString(ciphertext), // Only ciphertext, no IV - Key: base64.StdEncoding.EncodeToString(ephemeralKeyDER), - }, nil -} diff --git a/notehub/lib/handlers.go b/notehub/lib/handlers.go deleted file mode 100644 index fce3f27..0000000 --- a/notehub/lib/handlers.go +++ /dev/null @@ -1,1011 +0,0 @@ -package lib - -import ( - "context" - "encoding/json" - "fmt" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/mark3labs/mcp-go/mcp" -) - -// HandleProjectListTool handles listing Notehub projects -func HandleProjectListTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - response, err := MakeNotehubAPIRequest("GET", "/v1/projects", nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list projects: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleProjectCreateTool handles creating a new Notehub project -func HandleProjectCreateTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectName, err := request.RequireString("name") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid name parameter: %v", err)), nil - } - - billingAccountUID, err := request.RequireString("billing_account_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid billing_account_uid parameter: %v", err)), nil - } - - projectDescription := request.GetString("description", "") - - projectPayload := map[string]interface{}{ - "label": projectName, - "billing_account_uid": billingAccountUID, - } - - if projectDescription != "" { - projectPayload["description"] = projectDescription - } - - payloadBytes, err := json.Marshal(projectPayload) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal project payload: %v", err)), nil - } - - response, err := MakeNotehubAPIRequest("POST", "/v1/projects", payloadBytes) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create project: %v", err)), nil - } - - var responseData map[string]interface{} - if err := json.Unmarshal([]byte(response), &responseData); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse project creation response: %v", err)), nil - } - - if uid, exists := responseData["uid"]; exists { - projectURL := fmt.Sprintf("https://notehub.io/project/%s", uid) - responseData["project_url"] = projectURL - } else { - return mcp.NewToolResultError("Failed to extract project UID from response"), nil - } - - modifiedResponse, err := json.Marshal(responseData) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal modified response: %v", err)), nil - } - - return mcp.NewToolResultText(string(modifiedResponse)), nil -} - -// HandleProjectDetailTool handles getting detailed information about a specific project -func HandleProjectDetailTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s", projectUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get project details: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleDeviceListTool handles listing devices in a Notehub project with optional filtering and pagination -func HandleDeviceListTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - // Extract optional parameters - pageSize := request.GetInt("pageSize", 0) - pageNum := request.GetInt("pageNum", 0) - deviceUIDs := request.GetStringSlice("deviceUID", []string{}) - tags := request.GetStringSlice("tags", []string{}) - serialNumbers := request.GetStringSlice("serialNumber", []string{}) - fleetUIDs := request.GetStringSlice("fleetUID", []string{}) - notecardFirmware := request.GetString("notecardFirmware", "") - location := request.GetString("location", "") - hostFirmware := request.GetString("hostFirmware", "") - productUIDs := request.GetStringSlice("productUID", []string{}) - skus := request.GetStringSlice("sku", []string{}) - - params := url.Values{} - - if pageSize > 0 { - params.Add("pageSize", strconv.Itoa(pageSize)) - } - if pageNum > 0 { - params.Add("pageNum", strconv.Itoa(pageNum)) - } - - // Add device UID filters - for _, deviceUID := range deviceUIDs { - if deviceUID := strings.TrimSpace(deviceUID); deviceUID != "" { - params.Add("deviceUID", deviceUID) - } - } - - // Add tag filters - for _, tag := range tags { - if tag := strings.TrimSpace(tag); tag != "" { - params.Add("tag", tag) - } - } - - // Add serial number filters - for _, serialNumber := range serialNumbers { - if serialNumber := strings.TrimSpace(serialNumber); serialNumber != "" { - params.Add("serialNumber", serialNumber) - } - } - - // Add fleet UID filters - for _, fleetUID := range fleetUIDs { - if fleetUID := strings.TrimSpace(fleetUID); fleetUID != "" { - params.Add("fleetUID", fleetUID) - } - } - - // Add additional filters - if notecardFirmware != "" { - params.Add("notecardFirmware", notecardFirmware) - } - if location != "" { - params.Add("location", location) - } - if hostFirmware != "" { - params.Add("hostFirmware", hostFirmware) - } - - // Add product UID filters - for _, productUID := range productUIDs { - if productUID := strings.TrimSpace(productUID); productUID != "" { - params.Add("productUID", productUID) - } - } - - // Add SKU filters - for _, sku := range skus { - if sku := strings.TrimSpace(sku); sku != "" { - params.Add("sku", sku) - } - } - - queryString := "" - if len(params) > 0 { - queryString = "?" + params.Encode() - } - - endpoint := fmt.Sprintf("/v1/projects/%s/devices%s", projectUID, queryString) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list devices: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleProjectEventsTool handles listing events in a Notehub project -func HandleProjectEventsTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - // Extract optional parameters - pageSize := request.GetInt("pageSize", 0) - pageNum := request.GetInt("pageNum", 0) - deviceUIDs := request.GetStringSlice("deviceUID", []string{}) - sortBy := request.GetString("sortBy", "") - sortOrder := request.GetString("sortOrder", "") - startDate := request.GetString("startDate", "") - endDate := request.GetString("endDate", "") - dateType := request.GetString("dateType", "") - systemFilesOnly := request.GetString("systemFilesOnly", "") - files := request.GetString("files", "") - format := request.GetString("format", "") - serialNumbers := request.GetStringSlice("serialNumber", []string{}) - fleetUIDs := request.GetStringSlice("fleetUID", []string{}) - sessionUIDs := request.GetStringSlice("sessionUID", []string{}) - eventUIDs := request.GetStringSlice("eventUID", []string{}) - selectFields := request.GetString("selectFields", "") - - params := url.Values{} - - if pageSize > 0 { - params.Add("pageSize", strconv.Itoa(pageSize)) - } - if pageNum > 0 { - params.Add("pageNum", strconv.Itoa(pageNum)) - } - - // Add device UID filters - for _, deviceUID := range deviceUIDs { - if deviceUID := strings.TrimSpace(deviceUID); deviceUID != "" { - params.Add("deviceUID", deviceUID) - } - } - - if sortBy != "" { - params.Add("sortBy", sortBy) - } - if sortOrder != "" { - params.Add("sortOrder", sortOrder) - } - if startDate != "" { - params.Add("startDate", startDate) - } - if endDate != "" { - params.Add("endDate", endDate) - } - if dateType != "" { - params.Add("dateType", dateType) - } - if systemFilesOnly != "" { - params.Add("systemFilesOnly", systemFilesOnly) - } - if files != "" { - params.Add("files", files) - } - if format != "" { - params.Add("format", format) - } - - // Add serial number filters - for _, serialNumber := range serialNumbers { - if serialNumber := strings.TrimSpace(serialNumber); serialNumber != "" { - params.Add("serialNumber", serialNumber) - } - } - - // Add fleet UID filters - for _, fleetUID := range fleetUIDs { - if fleetUID := strings.TrimSpace(fleetUID); fleetUID != "" { - params.Add("fleetUID", fleetUID) - } - } - - // Add session UID filters - for _, sessionUID := range sessionUIDs { - if sessionUID := strings.TrimSpace(sessionUID); sessionUID != "" { - params.Add("sessionUID", sessionUID) - } - } - - // Add event UID filters - for _, eventUID := range eventUIDs { - if eventUID := strings.TrimSpace(eventUID); eventUID != "" { - params.Add("eventUID", eventUID) - } - } - - if selectFields != "" { - params.Add("selectFields", selectFields) - } - - queryString := "" - if len(params) > 0 { - queryString = "?" + params.Encode() - } - - endpoint := fmt.Sprintf("/v1/projects/%s/events%s", projectUID, queryString) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list events: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleSendNoteTool handles sending a note to a device -func HandleSendNoteTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - deviceUID, err := request.RequireString("device_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid device_uid parameter: %v", err)), nil - } - - noteFile, err := request.RequireString("note_file") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid note_file parameter: %v", err)), nil - } - - noteBody, err := request.RequireString("note_body") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid note_body parameter: %v", err)), nil - } - - notePayload := map[string]interface{}{ - "body": json.RawMessage(noteBody), - } - - payloadBytes, err := json.Marshal(notePayload) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal note payload: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/devices/%s/notes/%s", projectUID, deviceUID, noteFile) - response, err := MakeNotehubAPIRequest("POST", endpoint, payloadBytes) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to send note: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleRouteListTool handles listing routes in a Notehub project -func HandleRouteListTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/routes", projectUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list routes: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleRouteDetailTool handles getting detailed information about a specific route -func HandleRouteDetailTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - routeUID, err := request.RequireString("route_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid route_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/routes/%s", projectUID, routeUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get route details: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleDeviceHealthLogTool handles getting device health log information -func HandleDeviceHealthLogTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - deviceUID, err := request.RequireString("device_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid device_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/devices/%s/health-log", projectUID, deviceUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get device health log: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleMonitorListTool handles listing monitors in a Notehub project -func HandleMonitorListTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/monitors", projectUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list monitors: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleMonitorDetailTool handles getting detailed information about a specific monitor -func HandleMonitorDetailTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - monitorUID, err := request.RequireString("monitor_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid monitor_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/monitors/%s", projectUID, monitorUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get monitor details: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleDevicePublicKeyTool handles getting device public key information -func HandleDevicePublicKeyTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - deviceUID, err := request.RequireString("device_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid device_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/devices/%s/public-key", projectUID, deviceUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get device public key: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleSendEncryptedNoteTool handles sending encrypted notes to devices -func HandleSendEncryptedNoteTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - deviceUID, err := request.RequireString("device_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid device_uid parameter: %v", err)), nil - } - - noteFile, err := request.RequireString("note_file") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid note_file parameter: %v", err)), nil - } - - plaintextMessage, err := request.RequireString("plaintext_message") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid plaintext_message parameter: %v", err)), nil - } - - // Get the device's public key - publicKeyEndpoint := fmt.Sprintf("/v1/projects/%s/devices/%s/public-key", projectUID, deviceUID) - publicKeyResponse, err := MakeNotehubAPIRequest("GET", publicKeyEndpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get device public key: %v", err)), nil - } - - // Parse the public key response - var keyData struct { - Key string `json:"key"` - } - if err := json.Unmarshal([]byte(publicKeyResponse), &keyData); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse public key response: %v", err)), nil - } - - // Encrypt the message - encryptedNote, err := EncryptMessage(keyData.Key, []byte(plaintextMessage)) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to encrypt message: %v", err)), nil - } - - // Create the encrypted note payload - notePayload := map[string]interface{}{ - "body": encryptedNote, - } - - payloadBytes, err := json.Marshal(notePayload) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal encrypted note payload: %v", err)), nil - } - - sendEndpoint := fmt.Sprintf("/v1/projects/%s/devices/%s/notes/%s", projectUID, deviceUID, noteFile) - response, err := MakeNotehubAPIRequest("POST", sendEndpoint, payloadBytes) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to send encrypted note: %v", err)), nil - } - - return mcp.NewToolResultText(fmt.Sprintf("Encrypted note sent successfully. Response: %s", response)), nil -} - -// HandleBillingAccountListTool handles listing billing accounts -func HandleBillingAccountListTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - response, err := MakeNotehubAPIRequest("GET", "/v1/billing-accounts", nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list billing accounts: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleProductCreateTool handles creating a new product in a Notehub project -func HandleProductCreateTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - productUID, err := request.RequireString("product_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid product_uid parameter: %v", err)), nil - } - - label, err := request.RequireString("label") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid label parameter: %v", err)), nil - } - - autoProvisionFleets := request.GetString("auto_provision_fleets", "") - disableDevicesByDefault := request.GetString("disable_devices_by_default", "") - - productPayload := map[string]interface{}{ - "product_uid": productUID, - "label": label, - } - - if autoProvisionFleets != "" { - productPayload["auto_provision_fleets"] = autoProvisionFleets - } - - if disableDevicesByDefault != "" { - productPayload["disable_devices_by_default"] = disableDevicesByDefault - } - - payloadBytes, err := json.Marshal(productPayload) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal product payload: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/products", projectUID) - response, err := MakeNotehubAPIRequest("POST", endpoint, payloadBytes) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to create product: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleProductListTool handles listing products in a Notehub project -func HandleProductListTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/products", projectUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list products: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleEnvironmentVariablesSetTool handles setting environment variables at device, fleet, or project scope -func HandleEnvironmentVariablesSetTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - scope, err := request.RequireString("scope") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid scope parameter: %v", err)), nil - } - - environmentVariables, err := request.RequireString("environment_variables") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid environment_variables parameter: %v", err)), nil - } - - var endpoint string - - switch scope { - case "device": - uid, err := request.RequireString("uid") - if err != nil { - return mcp.NewToolResultError("uid is required when scope is 'device'"), nil - } - endpoint = fmt.Sprintf("/v1/projects/%s/devices/%s/environment_variables", projectUID, uid) - case "fleet": - uid, err := request.RequireString("uid") - if err != nil { - return mcp.NewToolResultError("uid is required when scope is 'fleet'"), nil - } - endpoint = fmt.Sprintf("/v1/projects/%s/fleets/%s/environment_variables", projectUID, uid) - case "project": - endpoint = fmt.Sprintf("/v1/projects/%s/environment_variables", projectUID) - default: - return mcp.NewToolResultError("Invalid scope. Must be 'device', 'fleet', or 'project'"), nil - } - - var envVars map[string]interface{} - if err := json.Unmarshal([]byte(environmentVariables), &envVars); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid environment_variables JSON: %v", err)), nil - } - - payload := map[string]interface{}{ - "environment_variables": envVars, - } - - payloadBytes, err := json.Marshal(payload) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal environment variables payload: %v", err)), nil - } - - response, err := MakeNotehubAPIRequest("PUT", endpoint, payloadBytes) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to set environment variables: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleFleetListTool handles listing fleets in a Notehub project -func HandleFleetListTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/fleets", projectUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to list fleets: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleFleetGetTool handles getting detailed information about a specific fleet -func HandleFleetGetTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - fleetUID, err := request.RequireString("fleet_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid fleet_uid parameter: %v", err)), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/fleets/%s", projectUID, fleetUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get fleet details: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleDeviceDfuHistoryTool handles getting device DFU history -func HandleDeviceDfuHistoryTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - deviceUID, err := request.RequireString("device_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid device_uid parameter: %v", err)), nil - } - - firmwareType, err := request.RequireString("firmware_type") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid firmware_type parameter: %v", err)), nil - } - - if firmwareType != "host" && firmwareType != "notecard" { - return mcp.NewToolResultError("Invalid firmware_type. Must be 'host' or 'notecard'"), nil - } - - endpoint := fmt.Sprintf("/v1/projects/%s/devices/%s/dfu/%s/history", projectUID, deviceUID, firmwareType) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get device DFU history: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleFirmwareHostUploadTool handles uploading host firmware binary to Notehub -func HandleFirmwareHostUploadTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - filePath, err := request.RequireString("file_path") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid file_path parameter: %v", err)), nil - } - - // Extract optional parameters - org := request.GetString("org", "") - product := request.GetString("product", "") - firmware := request.GetString("firmware", "") - version := request.GetString("version", "") - target := request.GetString("target", "") - versionString := request.GetString("version_string", "") - built := request.GetString("built", "") - builder := request.GetString("builder", "") - - // Parse version string into components if provided - var verMajor, verMinor, verPatch int32 - if versionString != "" { - parts := strings.Split(versionString, ".") - if len(parts) != 3 { - return mcp.NewToolResultError(fmt.Sprintf("Invalid version_string format '%s'. Expected format: 'major.minor.patch' (e.g., '1.2.3')", versionString)), nil - } - - versions := []*int32{&verMajor, &verMinor, &verPatch} - versionNames := []string{"major", "minor", "patch"} - - for i, part := range parts { - val, err := strconv.Atoi(part) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid %s version '%s' in version_string '%s'. Must be a number", versionNames[i], part, versionString)), nil - } - *versions[i] = int32(val) - } - } - - // Read the file from the filesystem - binaryBytes, err := os.ReadFile(filePath) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to read file from path '%s': %v", filePath, err)), nil - } - - filename := filepath.Base(filePath) - - endpoint := fmt.Sprintf("/v1/projects/%s/firmware/host/%s", projectUID, filename) - - // Add query parameters if provided - queryParams := []string{} - - if org != "" { - queryParams = append(queryParams, fmt.Sprintf("org=%s", url.QueryEscape(org))) - } - if product != "" { - queryParams = append(queryParams, fmt.Sprintf("product=%s", url.QueryEscape(product))) - } - if firmware != "" { - queryParams = append(queryParams, fmt.Sprintf("firmware=%s", url.QueryEscape(firmware))) - } - if version != "" { - queryParams = append(queryParams, fmt.Sprintf("version=%s", url.QueryEscape(version))) - } - if target != "" { - queryParams = append(queryParams, fmt.Sprintf("target=%s", url.QueryEscape(target))) - } - if versionString != "" { - queryParams = append(queryParams, fmt.Sprintf("ver_major=%d", verMajor)) - queryParams = append(queryParams, fmt.Sprintf("ver_minor=%d", verMinor)) - queryParams = append(queryParams, fmt.Sprintf("ver_patch=%d", verPatch)) - } - if built != "" { - queryParams = append(queryParams, fmt.Sprintf("built=%s", url.QueryEscape(built))) - } - if builder != "" { - queryParams = append(queryParams, fmt.Sprintf("builder=%s", url.QueryEscape(builder))) - } - - // Append query parameters to endpoint if any exist - if len(queryParams) > 0 { - endpoint += "?" + strings.Join(queryParams, "&") - } - - response, err := MakeNotehubAPIRequest("PUT", endpoint, binaryBytes) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to upload host firmware: %v", err)), nil - } - - return mcp.NewToolResultText(response), nil -} - -// HandleCheckNotefilesTool handles checking for Notefiles sent to Notehub -func HandleCheckNotefilesTool(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) { - if SessionToken == "" { - return mcp.NewToolResultError("No session token available. Please refresh token first."), nil - } - - projectUID, err := request.RequireString("project_uid") - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Invalid project_uid parameter: %v", err)), nil - } - - deviceUID := request.GetString("device_uid", "") - - // Get project events - endpoint := fmt.Sprintf("/v1/projects/%s/events", projectUID) - response, err := MakeNotehubAPIRequest("GET", endpoint, nil) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to get project events: %v", err)), nil - } - - // Parse the response to filter for Notefiles - var eventsData struct { - Events []map[string]interface{} `json:"events"` - } - if err := json.Unmarshal([]byte(response), &eventsData); err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to parse events response: %v", err)), nil - } - - // Filter events to show only Notefile-related events - var notefileEvents []map[string]interface{} - for _, event := range eventsData.Events { - // Check if this event has a file field (indicates a Notefile) - if file, exists := event["file"]; exists && file != nil { - fileStr, ok := file.(string) - if !ok { - continue - } - - // Filter by device if specified - if deviceUID != "" { - if eventDeviceUID, exists := event["device_uid"]; exists { - if eventDeviceUID != deviceUID { - continue - } - } - } - - // Check if it's a Notefile pattern (.qo, .dbo, etc.) - if strings.HasSuffix(fileStr, ".qo") || strings.HasSuffix(fileStr, ".dbo") || - strings.HasSuffix(fileStr, ".qi") || strings.HasSuffix(fileStr, ".dbi") || - strings.Contains(fileStr, "_session") || strings.Contains(fileStr, "_health") { - notefileEvents = append(notefileEvents, event) - } - } - } - - // Create a summary response - var result struct { - Summary string `json:"summary"` - TotalEvents int `json:"total_events"` - NotefileEvents int `json:"notefile_events"` - ProjectUID string `json:"project_uid"` - DeviceUID string `json:"device_uid,omitempty"` - Events []map[string]interface{} `json:"events"` - CommonFiles map[string]int `json:"common_files"` - Troubleshooting []string `json:"troubleshooting"` - } - - result.ProjectUID = projectUID - if deviceUID != "" { - result.DeviceUID = deviceUID - } - result.TotalEvents = len(eventsData.Events) - result.NotefileEvents = len(notefileEvents) - result.Events = notefileEvents - - // Count common file types - result.CommonFiles = make(map[string]int) - for _, event := range notefileEvents { - if file, exists := event["file"]; exists { - if fileStr, ok := file.(string); ok { - result.CommonFiles[fileStr]++ - } - } - } - - // Create summary - if len(notefileEvents) == 0 { - result.Summary = "No Notefiles found in recent events. This could mean your Notecard hasn't sent data yet, or the data was sent earlier than the current event window." - result.Troubleshooting = []string{ - "Check if your Notecard is properly provisioned and connected", - "Verify the Notecard has synced with Notehub (check hub.status)", - "Ensure your Notecard application is sending data to Notefiles", - "Check device health logs for connection issues", - "Verify your Notecard has the correct project UID configured", - } - } else { - deviceText := "" - if deviceUID != "" { - deviceText = fmt.Sprintf(" from device %s", deviceUID) - } - result.Summary = fmt.Sprintf("Found %d Notefile events%s out of %d total events in project %s", - len(notefileEvents), deviceText, len(eventsData.Events), projectUID) - } - - resultBytes, err := json.Marshal(result) - if err != nil { - return mcp.NewToolResultError(fmt.Sprintf("Failed to marshal result: %v", err)), nil - } - - return mcp.NewToolResultText(string(resultBytes)), nil -} diff --git a/notehub/main.go b/notehub/main.go deleted file mode 100644 index 84a3916..0000000 --- a/notehub/main.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - - "note-mcp/notehub/lib" - "note-mcp/utils" - - "github.com/mark3labs/mcp-go/server" -) - -var ( - envFilePath string -) - -func init() { - flag.StringVar(&envFilePath, "env", ".env", "Path to .env file") -} - -func main() { - flag.Parse() - - // Load credentials from .env file - credentials, err := lib.GetNotehubCredentials(envFilePath) - if err != nil { - log.Printf("Warning: Failed to load Notehub credentials: %v", err) - } else { - sessionToken, err := lib.CreateSessionToken(credentials.Username, credentials.Password) - if err != nil { - log.Printf("Warning: Failed to create session token: %v", err) - } else { - lib.SessionToken = sessionToken - } - } - - s := server.NewMCPServer( - "Notehub MCP", - utils.Commit, - server.WithToolCapabilities(true), - server.WithRecovery(), - ) - - // Create and register Notehub tools - projectListTool := CreateProjectListTool() - projectCreateTool := CreateProjectCreateTool() - projectDetailTool := CreateProjectDetailTool() - deviceListTool := CreateDeviceListTool() - projectEventsTool := CreateProjectEventsTool() - checkNotefilesTool := CreateCheckNotefilesTool() - sendNoteTool := CreateSendNoteTool() - sendEncryptedNoteTool := CreateSendEncryptedNoteTool() - routeListTool := CreateRouteListTool() - routeDetailTool := CreateRouteDetailTool() - deviceHealthLogTool := CreateDeviceHealthLogTool() - monitorListTool := CreateMonitorListTool() - monitorDetailTool := CreateMonitorDetailTool() - devicePublicKeyTool := CreateDevicePublicKeyTool() - billingAccountListTool := CreateBillingAccountListTool() - productCreateTool := CreateProductCreateTool() - productListTool := CreateProductListTool() - environmentVariablesSetTool := CreateEnvironmentVariablesSetTool() - fleetListTool := CreateFleetListTool() - fleetGetTool := CreateFleetGetTool() - deviceDfuHistoryTool := CreateDeviceDfuHistoryTool() - firmwareHostUploadTool := CreateFirmwareHostUploadTool() - - // Add tool handlers from lib package - s.AddTool(projectListTool, lib.HandleProjectListTool) - s.AddTool(projectCreateTool, lib.HandleProjectCreateTool) - s.AddTool(projectDetailTool, lib.HandleProjectDetailTool) - s.AddTool(deviceListTool, lib.HandleDeviceListTool) - s.AddTool(projectEventsTool, lib.HandleProjectEventsTool) - s.AddTool(checkNotefilesTool, lib.HandleCheckNotefilesTool) - s.AddTool(sendNoteTool, lib.HandleSendNoteTool) - s.AddTool(sendEncryptedNoteTool, lib.HandleSendEncryptedNoteTool) - s.AddTool(routeListTool, lib.HandleRouteListTool) - s.AddTool(routeDetailTool, lib.HandleRouteDetailTool) - s.AddTool(deviceHealthLogTool, lib.HandleDeviceHealthLogTool) - s.AddTool(monitorListTool, lib.HandleMonitorListTool) - s.AddTool(monitorDetailTool, lib.HandleMonitorDetailTool) - s.AddTool(devicePublicKeyTool, lib.HandleDevicePublicKeyTool) - s.AddTool(billingAccountListTool, lib.HandleBillingAccountListTool) - s.AddTool(productCreateTool, lib.HandleProductCreateTool) - s.AddTool(productListTool, lib.HandleProductListTool) - s.AddTool(environmentVariablesSetTool, lib.HandleEnvironmentVariablesSetTool) - s.AddTool(fleetListTool, lib.HandleFleetListTool) - s.AddTool(fleetGetTool, lib.HandleFleetGetTool) - s.AddTool(deviceDfuHistoryTool, lib.HandleDeviceDfuHistoryTool) - s.AddTool(firmwareHostUploadTool, lib.HandleFirmwareHostUploadTool) - - if err := server.ServeStdio(s); err != nil { - fmt.Printf("Server error: %v\n", err) - } -} diff --git a/notehub/tools.go b/notehub/tools.go deleted file mode 100644 index 1aa2f14..0000000 --- a/notehub/tools.go +++ /dev/null @@ -1,537 +0,0 @@ -// Package main provides MCP (Model Context Protocol) tools for interacting with the Notehub API. -// -// This package implements a comprehensive set of tools for managing Blues Wireless Notehub -// projects, devices, routes, monitors, and encrypted communications. -// -// Available Tools: -// - project_list: List all Notehub projects -// - project_create: Create a new Notehub project -// - device_list: List devices in a project (with optional tag filtering) -// - device_health_log: Get device health information -// - device_public_key: Get device public key for encryption -// - project_events: List events in a project -// - check_notefiles: Check for Notefiles sent to Notehub by filtering project events -// - route_list: List routes in a project -// - route_detail: Get detailed route information -// - monitor_list: List monitors in a project -// - monitor_detail: Get detailed monitor information -// - send_note: Send a note to a device -// - send_encrypted_note: Send an encrypted note using device public key -// - billing_account_list: List all billing accounts -// - product_create: Create a new product in a project -// - product_list: List all products in a project -// - environment_variables_set: Set environment variables at device, fleet, or project scope -// - fleet_list: List all fleets in a project -// - fleet_get: Get detailed information about a specific fleet -// - device_dfu_history: Get device DFU history for host or notecard firmware -// - firmware_host_upload: Upload host firmware binary to a project -// -// Authentication: -// -// Uses session token authentication via NOTEHUB_USER and NOTEHUB_PASS environment variables. -// Tokens are automatically refreshed as needed. -// -// API Base URL: https://api.notefile.net -// -// Example .env file: -// -// NOTEHUB_USER=your@email.com -// NOTEHUB_PASS=your_password -package main - -import ( - "github.com/mark3labs/mcp-go/mcp" -) - -// CreateProjectListTool creates a tool for listing Notehub projects -func CreateProjectListTool() mcp.Tool { - return mcp.NewTool("project_list", - mcp.WithDescription("List all Notehub projects belonging to the authenticated user"), - ) -} - -// CreateProjectDetailTool creates a tool for getting detailed information about a specific project -func CreateProjectDetailTool() mcp.Tool { - return mcp.NewTool("project_detail", - mcp.WithDescription("Get detailed information about a specific project in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to get details for"), - ), - ) -} - -// CreateProjectCreateTool creates a tool for creating a new Notehub project -func CreateProjectCreateTool() mcp.Tool { - return mcp.NewTool("project_create", - mcp.WithDescription("Create a new Notehub project. You will need to have a billing account to create a project. You should typically follow up by issuing a 'product_create' request to create a new product in the project, as a Product UID is needed to provision a Notecard."), - mcp.WithString("name", - mcp.Required(), - mcp.Description("The name of the project to create"), - ), - mcp.WithString("billing_account_uid", - mcp.Required(), - mcp.Description("The UID of the billing account to associate with the project"), - ), - mcp.WithString("description", - mcp.Description("Optional description for the project"), - ), - ) -} - -// CreateDeviceListTool creates a tool for listing devices in a Notehub project. -// -// This tool provides access to the Notehub API endpoint: -// GET /v1/projects/{projectUID}/devices -// -// Parameters: -// - project_uid (required): The UID of the project to list devices for -// - pageSize (optional): Number of devices to return per page (default 50) -// - pageNum (optional): Page number of results (must be >= 1, default 1) -// - deviceUID (optional): Array of specific device UIDs to filter by -// - tags (optional): Array of tags to filter devices by -// - serialNumber (optional): Array of serial numbers to filter devices by -// - fleetUID (optional): Fleet UID to filter devices by -// - notecardFirmware (optional): Notecard firmware version to filter by -// - location (optional): Location to filter devices by -// - hostFirmware (optional): Host firmware version to filter by -// - productUID (optional): Product UID to filter devices by -// - sku (optional): SKU to filter devices by -// -// Example usage: -// - List all devices: {"project_uid": "app:123..."} -// - Filter by tags: {"project_uid": "app:123...", "tags": ["production", "sensor"]} -// - Filter by device UIDs: {"project_uid": "app:123...", "deviceUID": ["dev:123...", "dev:456..."]} -// - Filter by fleet: {"project_uid": "app:123...", "fleetUID": "fleet:123..."} -// - Paginated results: {"project_uid": "app:123...", "pageSize": 25, "pageNum": 2} -// -// Returns: -// -// JSON array of device objects with their metadata and status information. -func CreateDeviceListTool() mcp.Tool { - return mcp.NewTool("device_list", - mcp.WithDescription("List all devices in a specific Notehub project with optional filtering and pagination"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to list devices for (format: app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)"), - ), - mcp.WithNumber("pageSize", - mcp.Description("Optional number of devices to return per page (defaults to 50, try larger values if you have a lot of devices)"), - ), - mcp.WithNumber("pageNum", - mcp.Description("Optional page number of results (must be >= 1, defaults to 1)"), - ), - mcp.WithArray("deviceUID", - mcp.Description("Optional array of specific device UIDs to filter by. Example: ['dev:864475012345678', 'dev:864622087654321']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithArray("tags", - mcp.Description("Optional array of tags to filter devices by. Example: ['production', 'sensor', 'outdoor']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithArray("serialNumber", - mcp.Description("Optional array of device serial numbers to filter by. Example: ['SN001', 'SN002']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithArray("fleetUID", - mcp.Description("Optional array of fleet UIDs to filter devices by. Example: ['fleet:00000000-0000-0000-0000-000000000001', 'fleet:00000000-0000-0000-0000-000000000002']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithString("notecardFirmware", - mcp.Description("Optional Notecard firmware version to filter devices by (e.g., '6.2.1.15266')"), - ), - mcp.WithString("location", - mcp.Description("Optional location to filter devices by"), - ), - mcp.WithString("hostFirmware", - mcp.Description("Optional host firmware version to filter devices by"), - ), - mcp.WithArray("productUID", - mcp.Description("Optional array of product UIDs to filter devices by. Example: ['com.company.user:product1', 'com.company.user:product2']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithArray("sku", - mcp.Description("Optional array of SKUs to filter devices by. Example: ['NOTE-WBEX', 'NOTE-NBGL-500']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - ) -} - -// CreateProjectEventsTool creates a tool for listing events in a Notehub project -func CreateProjectEventsTool() mcp.Tool { - return mcp.NewTool("project_events", - mcp.WithDescription("List all events in a specific Notehub project. Use optional parameters to filter the results."), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to list events for"), - ), - mcp.WithNumber("pageSize", - mcp.Description("Optional number of events to return per page"), - ), - mcp.WithNumber("pageNum", - mcp.Description("Optional page number of results (must be >= 1)"), - ), - mcp.WithArray("deviceUID", - mcp.Description("Optional array of device UIDs to filter events for specific devices. Example: ['dev:123456789', 'dev:987654321']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithString("sortBy", - mcp.Description("Optional field to sort results by. Valid values are: best_id, device_serial, device_uid, captured, modified, device_location, tower_location, triangulated_location, best_location"), - ), - mcp.WithString("sortOrder", - mcp.Description("Optional sort order: 'asc' or 'desc'"), - ), - mcp.WithString("startDate", - mcp.Description("Optional start date for filtering events (ISO 8601 format)"), - ), - mcp.WithString("endDate", - mcp.Description("Optional end date for filtering events (ISO 8601 format)"), - ), - mcp.WithString("dateType", - mcp.Description("Optional date type for filtering: 'captured' or 'when'"), - ), - mcp.WithString("systemFilesOnly", - mcp.Description("Optional filter to show only system files: 'true' or 'false'"), - ), - mcp.WithString("files", - mcp.Description("Optional comma-separated list of notefiles to filter by, e.g. 'data.qo,sensors.qo'"), - ), - mcp.WithString("format", - mcp.Description("Optional response format: 'json' or 'csv'"), - ), - mcp.WithArray("serialNumber", - mcp.Description("Optional array of device serial numbers to filter events. Example: ['SN001', 'SN002']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithArray("fleetUID", - mcp.Description("Optional array of fleet UIDs to filter events for devices in specific fleets. Example: ['fleet:00000000-0000-0000-0000-000000000001', 'fleet:00000000-0000-0000-0000-000000000002']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithArray("sessionUID", - mcp.Description("Optional array of session UIDs to filter events for specific sessions. Example: ['00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithArray("eventUID", - mcp.Description("Optional array of event UIDs to get specific events. Example: ['00000000-0000-0000-0000-000000000001', '00000000-0000-0000-0000-000000000002']"), - mcp.Items(map[string]any{ - "type": "string", - }), - ), - mcp.WithString("selectFields", - mcp.Description("Optional comma-separated list of fields to include in the response"), - ), - ) -} - -// CreateCheckNotefilesTool creates a tool for checking Notefiles sent to Notehub -func CreateCheckNotefilesTool() mcp.Tool { - return mcp.NewTool("check_notefiles", - mcp.WithDescription("Check for Notefiles that have been sent to Notehub by filtering project events to show only Notefile-related data"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to check for Notefiles"), - ), - mcp.WithString("device_uid", - mcp.Description("Optional: The specific device UID to filter for (e.g., 'dev:123456789'). If not provided, will show Notefiles from all devices in the project."), - ), - ) -} - -// CreateSendNoteTool creates a tool for sending a note to a device -func CreateSendNoteTool() mcp.Tool { - return mcp.NewTool("send_note", - mcp.WithDescription("Send a note to a specific device in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project containing the target device"), - ), - mcp.WithString("device_uid", - mcp.Required(), - mcp.Description("The UID of the device to send the note to"), - ), - mcp.WithString("note_file", - mcp.Required(), - mcp.Description("The note file name, it should always be of *.qi or *.db as it is an inbound note (e.g., 'data.qi' for an incoming queue or 'data.db' for a bidirectionally synchronized database). If the user wishes enabled TLS encryption, the note file name should be of *.qis or *.dbs."), - ), - mcp.WithString("note_body", - mcp.Required(), - mcp.Description("The JSON body content of the note"), - ), - ) -} - -// CreateRouteListTool creates a tool for listing routes in a Notehub project -func CreateRouteListTool() mcp.Tool { - return mcp.NewTool("route_list", - mcp.WithDescription("List all routes in a specific Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to list routes for"), - ), - ) -} - -// CreateRouteDetailTool creates a tool for getting detailed information about a specific route -func CreateRouteDetailTool() mcp.Tool { - return mcp.NewTool("route_detail", - mcp.WithDescription("Get detailed information about a specific route in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project containing the route"), - ), - mcp.WithString("route_uid", - mcp.Required(), - mcp.Description("The UID of the route to get details for"), - ), - ) -} - -// CreateDeviceHealthLogTool creates a tool for getting device health log information -func CreateDeviceHealthLogTool() mcp.Tool { - return mcp.NewTool("device_health_log", - mcp.WithDescription("Get device health log information for a specific device in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project containing the device"), - ), - mcp.WithString("device_uid", - mcp.Required(), - mcp.Description("The UID of the device to get health log for"), - ), - ) -} - -// CreateMonitorListTool creates a tool for listing monitors in a Notehub project -func CreateMonitorListTool() mcp.Tool { - return mcp.NewTool("monitor_list", - mcp.WithDescription("List all monitors in a specific Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to list monitors for"), - ), - ) -} - -// CreateMonitorDetailTool creates a tool for getting detailed information about a specific monitor -func CreateMonitorDetailTool() mcp.Tool { - return mcp.NewTool("monitor_detail", - mcp.WithDescription("Get detailed information about a specific monitor in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project containing the monitor"), - ), - mcp.WithString("monitor_uid", - mcp.Required(), - mcp.Description("The UID of the monitor to get details for"), - ), - ) -} - -// CreateDevicePublicKeyTool creates a tool for getting device public key information -func CreateDevicePublicKeyTool() mcp.Tool { - return mcp.NewTool("device_public_key", - mcp.WithDescription("Get device public key information for a specific device in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project containing the device"), - ), - mcp.WithString("device_uid", - mcp.Required(), - mcp.Description("The UID of the device to get public key for"), - ), - ) -} - -// CreateSendEncryptedNoteTool creates a tool for sending encrypted notes to devices -func CreateSendEncryptedNoteTool() mcp.Tool { - return mcp.NewTool("send_encrypted_note", - mcp.WithDescription("Send an encrypted note to a specific device using its public key"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project containing the target device"), - ), - mcp.WithString("device_uid", - mcp.Required(), - mcp.Description("The UID of the device to send the encrypted note to"), - ), - mcp.WithString("note_file", - mcp.Required(), - mcp.Description("The note file name, it should always be of *.qi or *.db as it is an inbound note (e.g., 'data.qi' for an incoming queue or 'data.db' for a bidirectionally synchronized database). If the user wishes enabled TLS encryption (in addition to encrypting the payload), the note file name should be of *.qis or *.dbs."), - ), - mcp.WithString("plaintext_message", - mcp.Required(), - mcp.Description("The plaintext message to encrypt and send"), - ), - ) -} - -// CreateBillingAccountListTool creates a tool for listing billing accounts -func CreateBillingAccountListTool() mcp.Tool { - return mcp.NewTool("billing_account_list", - mcp.WithDescription("List all billing accounts belonging to the authenticated user"), - ) -} - -// CreateProductCreateTool creates a tool for creating a new product in a Notehub project -func CreateProductCreateTool() mcp.Tool { - return mcp.NewTool("product_create", - mcp.WithDescription("Create a new product in a specific Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to create the product in"), - ), - mcp.WithString("product_uid", - mcp.Required(), - mcp.Description("The UID for the new product, (without the reverse URL prefix notation). For example, if the desired product UID is 'com.company.username:productname', the value should be 'productname'."), - ), - mcp.WithString("label", - mcp.Required(), - mcp.Description("The label/name/description of the new product"), - ), - mcp.WithString("auto_provision_fleets", - mcp.Description("Optional fleet UIDs for auto-provisioning (comma-separated)"), - ), - mcp.WithString("disable_devices_by_default", - mcp.Description("Optional: whether to disable devices by default (true/false)"), - ), - ) -} - -// CreateProductListTool creates a tool for listing products in a Notehub project -func CreateProductListTool() mcp.Tool { - return mcp.NewTool("product_list", - mcp.WithDescription("List all products in a specific Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to list products for"), - ), - ) -} - -// CreateEnvironmentVariablesSetTool creates a tool for setting environment variables at device, fleet, or project scope -func CreateEnvironmentVariablesSetTool() mcp.Tool { - return mcp.NewTool("environment_variables_set", - mcp.WithDescription("Set environment variables at device, fleet, or project scope in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project"), - ), - mcp.WithString("scope", - mcp.Required(), - mcp.Enum("device", "fleet", "project"), - mcp.Description("The scope for setting environment variables: 'device', 'fleet', or 'project'"), - ), - mcp.WithString("uid", - mcp.Description("The UID of the specified scope (e.g. device or fleet)"), - ), - mcp.WithString("environment_variables", - mcp.Required(), - mcp.Description("JSON string of environment variables to set (e.g., '{\"VAR1\":\"value1\",\"VAR2\":\"value2\"}')"), - ), - ) -} - -// CreateFleetListTool creates a tool for listing fleets in a Notehub project -func CreateFleetListTool() mcp.Tool { - return mcp.NewTool("fleet_list", - mcp.WithDescription("List all fleets in a specific Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to list fleets for"), - ), - ) -} - -// CreateFleetGetTool creates a tool for getting detailed information about a specific fleet -func CreateFleetGetTool() mcp.Tool { - return mcp.NewTool("fleet_get", - mcp.WithDescription("Get detailed information about a specific fleet in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project containing the fleet"), - ), - mcp.WithString("fleet_uid", - mcp.Required(), - mcp.Description("The UID of the fleet to get details for"), - ), - ) -} - -// CreateDeviceDfuHistoryTool creates a tool for getting device DFU history -func CreateDeviceDfuHistoryTool() mcp.Tool { - return mcp.NewTool("device_dfu_history", - mcp.WithDescription("Get device DFU (Device Firmware Update) history for a specific device in a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project containing the device"), - ), - mcp.WithString("device_uid", - mcp.Required(), - mcp.Description("The UID of the device to get DFU history for"), - ), - mcp.WithString("firmware_type", - mcp.Required(), - mcp.Enum("host", "notecard"), - mcp.Description("The type of firmware to get history for: 'host' or 'notecard'"), - ), - ) -} - -// CreateFirmwareHostUploadTool creates a tool for uploading host firmware binary to Notehub -func CreateFirmwareHostUploadTool() mcp.Tool { - return mcp.NewTool("firmware_host_upload", - mcp.WithDescription("Upload a host firmware binary file to a Notehub project"), - mcp.WithString("project_uid", - mcp.Required(), - mcp.Description("The UID of the project to upload firmware to"), - ), - mcp.WithString("file_path", - mcp.Required(), - mcp.Description("The local file system path to the firmware binary file (e.g., '/path/to/app-v1.0.0.bin')"), - ), - mcp.WithString("org", - mcp.Description("Optional organization identifier"), - ), - mcp.WithString("product", - mcp.Description("Optional product identifier"), - ), - mcp.WithString("firmware", - mcp.Description("Optional firmware identifier"), - ), - mcp.WithString("version", - mcp.Description("Optional version string"), - ), - mcp.WithString("target", - mcp.Description("Optional target identifier"), - ), - mcp.WithString("version_string", - mcp.Description("Optional version string (e.g., '1.2.3'). Will be parsed into major.minor.patch components"), - ), - mcp.WithString("built", - mcp.Description("Optional build timestamp"), - ), - mcp.WithString("builder", - mcp.Description("Optional builder identifier"), - ), - ) -} diff --git a/utils/cli.go b/utils/cli.go deleted file mode 100644 index 405db8d..0000000 --- a/utils/cli.go +++ /dev/null @@ -1,253 +0,0 @@ -package utils - -import ( - "bufio" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" -) - -// ExecuteArduinoCLICommand executes a arduino-cli command -func ExecuteArduinoCLICommand(args []string) (string, error) { - cmd := exec.Command("arduino-cli", args...) - output, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("failed to execute command: %v\nOutput: %s", err, string(output)) - } - return string(output), nil -} - -// ExecuteArduinoCLICommandWithLogger executes a arduino-cli command with logging support -// Provides enhanced logging and real-time output capture for any arduino-cli command -func ExecuteArduinoCLICommandWithLogger(args []string, logger *MCPLogger) (string, error) { - if logger != nil { - logger.Infof("Executing arduino-cli command: %v", args) - } - - cmd := exec.Command("arduino-cli", args...) - cmd.Env = os.Environ() - - // Create a user-writable temporary directory for arduino-cli operations - // This ensures arduino-cli has a safe place to write temporary files - var tempDir string - var shouldCleanup bool - - homeDir, err := os.UserHomeDir() - if err != nil { - if logger != nil { - logger.Warningf("Could not get user home directory: %v", err) - } - } else { - // Create a temporary directory in the user's home directory - tempDir = filepath.Join(homeDir, ".arduino-cli-temp") - if err := os.MkdirAll(tempDir, 0755); err != nil { - if logger != nil { - logger.Warningf("Could not create arduino-cli temp directory: %v", err) - } - } else { - // Set TMPDIR environment variable to point to our writable directory - cmd.Env = append(cmd.Env, "TMPDIR="+tempDir) - shouldCleanup = true - if logger != nil { - logger.Debugf("Set TMPDIR to: %s", tempDir) - } - } - } - - // Ensure cleanup of temporary directory after command completion - defer func() { - if shouldCleanup && tempDir != "" { - if err := os.RemoveAll(tempDir); err != nil { - if logger != nil { - logger.Warningf("Failed to clean up temporary directory %s: %v", tempDir, err) - } - } else { - if logger != nil { - logger.Debugf("Cleaned up temporary directory: %s", tempDir) - } - } - } - }() - - // Use streaming output to capture real-time progress for all commands - return executeWithStreamingLogging(cmd, logger, args) -} - -// ExecuteNotecardCommand executes a notecard command -func ExecuteNotecardCommand(args []string) (string, error) { - cmd := exec.Command("notecard", args...) - output, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("failed to execute command: %v\nOutput: %s", err, string(output)) - } - return string(output), nil -} - -// ExecuteNotecardCommandWithEnv executes a notecard command with custom environment variables -func ExecuteNotecardCommandWithEnv(args []string, env []string) (string, error) { - cmd := exec.Command("notecard", args...) - cmd.Env = env - output, err := cmd.CombinedOutput() - if err != nil { - return "", fmt.Errorf("failed to execute command: %v\nOutput: %s", err, string(output)) - } - return string(output), nil -} - -// ExecuteNotecardCommandWithLogger executes a notecard command with logging support -// Provides enhanced logging and real-time output capture for any notecard command -func ExecuteNotecardCommandWithLogger(args []string, logger *MCPLogger) (string, error) { - if logger != nil { - logger.Infof("Executing notecard command: %v", args) - } - - cmd := exec.Command("notecard", args...) - - // Use streaming output to capture real-time progress for all commands - return executeWithStreamingLogging(cmd, logger, args) -} - -// executeWithStreamingLogging handles any notecard command with real-time logging -func executeWithStreamingLogging(cmd *exec.Cmd, logger *MCPLogger, args []string) (string, error) { - // Determine command name for logging - commandName := "notecard" - if len(args) > 0 { - commandName = fmt.Sprintf("notecard %s", args[0]) - } - - if logger != nil { - logger.Info(fmt.Sprintf("Starting %s operation...", commandName)) - } - - // Get stdout and stderr pipes for real-time output - stdout, err := cmd.StdoutPipe() - if err != nil { - return "", fmt.Errorf("failed to create stdout pipe: %v", err) - } - - stderr, err := cmd.StderrPipe() - if err != nil { - return "", fmt.Errorf("failed to create stderr pipe: %v", err) - } - - // Start the command - if err := cmd.Start(); err != nil { - return "", fmt.Errorf("failed to start command: %v", err) - } - - // Channel to collect all output - outputChan := make(chan string, 100) - errorChan := make(chan error, 2) - - // Start goroutines to read stdout and stderr - go readAndLogCommandOutput(stdout, "stdout", logger, outputChan, errorChan) - go readAndLogCommandOutput(stderr, "stderr", logger, outputChan, errorChan) - - // Collect all output - var allOutput strings.Builder - outputDone := make(chan bool) - - go func() { - for output := range outputChan { - allOutput.WriteString(output) - allOutput.WriteString("\n") - } - outputDone <- true - }() - - // Wait for command to complete - cmdErr := cmd.Wait() - - // Close output channel and wait for collection to finish - close(outputChan) - <-outputDone - - // Check for read errors - select { - case readErr := <-errorChan: - if readErr != nil && logger != nil { - logger.Errorf("Error reading command output: %v", readErr) - } - default: - } - - if cmdErr != nil { - if logger != nil { - logger.Errorf("%s command failed: %v", commandName, cmdErr) - } - return allOutput.String(), fmt.Errorf("failed to execute command: %v\nOutput: %s", cmdErr, allOutput.String()) - } - - if logger != nil { - logger.Info(fmt.Sprintf("%s operation completed successfully", commandName)) - } - - return allOutput.String(), nil -} - -// readAndLogCommandOutput reads from a pipe and logs output for any command -func readAndLogCommandOutput(pipe interface{}, streamType string, logger *MCPLogger, outputChan chan<- string, errorChan chan<- error) { - scanner := bufio.NewScanner(pipe.(interface{ Read([]byte) (int, error) })) - - for scanner.Scan() { - line := scanner.Text() - outputChan <- line - - if logger != nil { - // Log the raw output for debugging - logger.Debugf("%s: %s", streamType, line) - - // Log significant events based on content - if isSignificantCommandEvent(line) { - logger.Infof("Command output: %s", line) - } - } - } - - if err := scanner.Err(); err != nil { - errorChan <- err - } -} - -// isSignificantCommandEvent determines if a log line represents a significant event for any command -func isSignificantCommandEvent(line string) bool { - line = strings.ToLower(strings.TrimSpace(line)) - - // Check for empty or very short lines - if len(line) < 3 { - return false - } - - significantKeywords := []string{ - "error", "failed", "success", "completed", "finished", - "starting", "progress", "warning", "info", - "connecting", "connected", "disconnected", - "timeout", "retry", "retrying", - "firmware", "version", "update", "download", - "transfer", "chunk", "bytes", "binary", - "waiting", "restarting", "ready", - "notecard", "response", "request", "side-loading", - } - - for _, keyword := range significantKeywords { - if strings.Contains(line, keyword) { - return true - } - } - - // Also consider lines with JSON-like content as significant - if (strings.Contains(line, "{") && strings.Contains(line, "}")) || - (strings.Contains(line, "[") && strings.Contains(line, "]")) { - return true - } - - // Consider lines with percentage or numeric progress - if strings.Contains(line, "%") || - (strings.Contains(line, "/") && (strings.Contains(line, "mb") || strings.Contains(line, "kb") || strings.Contains(line, "bytes"))) { - return true - } - - return false -} diff --git a/utils/logger.go b/utils/logger.go deleted file mode 100644 index 11a3897..0000000 --- a/utils/logger.go +++ /dev/null @@ -1,156 +0,0 @@ -package utils - -import ( - "fmt" - "time" - - "github.com/mark3labs/mcp-go/server" -) - -// LogLevel represents different logging levels -type LogLevel string - -const ( - LogLevelDebug LogLevel = "debug" - LogLevelInfo LogLevel = "info" - LogLevelWarning LogLevel = "warning" - LogLevelError LogLevel = "error" -) - -// MCPLogger provides logging capabilities for MCP servers -type MCPLogger struct { - server *server.MCPServer - loggerName string -} - -// NewMCPLogger creates a new MCP logger instance -func NewMCPLogger(server *server.MCPServer, loggerName string) *MCPLogger { - if loggerName == "" { - loggerName = "mcp-server" - } - - return &MCPLogger{ - server: server, - loggerName: loggerName, - } -} - -// SendLogNotification sends a log notification to all connected MCP clients -func (l *MCPLogger) SendLogNotification(level LogLevel, message string) error { - if l.server == nil { - return fmt.Errorf("MCP server not initialized") - } - - // Create MCP-compliant log notification - notification := map[string]interface{}{ - "level": string(level), - "logger": l.loggerName, - "data": message, - "timestamp": time.Now().Unix(), - } - - // Send to all connected clients - l.server.SendNotificationToAllClients( - "notifications/message", - notification, - ) - return nil -} - -// SendProgressNotification sends progress updates to MCP clients -func (l *MCPLogger) SendProgressNotification(progressToken string, progress, total float64, message string) error { - if l.server == nil { - return fmt.Errorf("MCP server not initialized") - } - - notification := map[string]interface{}{ - "progressToken": progressToken, - "progress": progress, - "total": total, - "message": message, - "timestamp": time.Now().Unix(), - } - - l.server.SendNotificationToAllClients( - "notifications/progress", - notification, - ) - return nil -} - -// SendCustomNotification sends a custom notification to MCP clients -func (l *MCPLogger) SendCustomNotification(notificationType string, data map[string]interface{}) error { - if l.server == nil { - return fmt.Errorf("MCP server not initialized") - } - - // Add timestamp if not already present - if _, exists := data["timestamp"]; !exists { - data["timestamp"] = time.Now().Unix() - } - - l.server.SendNotificationToAllClients(notificationType, data) - return nil -} - -// Debug logs a debug message -func (l *MCPLogger) Debug(message string) error { - return l.SendLogNotification(LogLevelDebug, message) -} - -// Debugf logs a formatted debug message -func (l *MCPLogger) Debugf(format string, args ...interface{}) error { - return l.Debug(fmt.Sprintf(format, args...)) -} - -// Info logs an info message -func (l *MCPLogger) Info(message string) error { - return l.SendLogNotification(LogLevelInfo, message) -} - -// Infof logs a formatted info message -func (l *MCPLogger) Infof(format string, args ...interface{}) error { - return l.Info(fmt.Sprintf(format, args...)) -} - -// Warning logs a warning message -func (l *MCPLogger) Warning(message string) error { - return l.SendLogNotification(LogLevelWarning, message) -} - -// Warningf logs a formatted warning message -func (l *MCPLogger) Warningf(format string, args ...interface{}) error { - return l.Warning(fmt.Sprintf(format, args...)) -} - -// Error logs an error message -func (l *MCPLogger) Error(message string) error { - return l.SendLogNotification(LogLevelError, message) -} - -// Errorf logs a formatted error message -func (l *MCPLogger) Errorf(format string, args ...interface{}) error { - return l.Error(fmt.Sprintf(format, args...)) -} - -// LogWithContext logs a message with additional context information -func (l *MCPLogger) LogWithContext(level LogLevel, message string, context map[string]interface{}) error { - if l.server == nil { - return fmt.Errorf("MCP server not initialized") - } - - // Create enhanced notification with context - notification := map[string]interface{}{ - "level": string(level), - "logger": l.loggerName, - "data": message, - "context": context, - "timestamp": time.Now().Unix(), - } - - l.server.SendNotificationToAllClients( - "notifications/message", - notification, - ) - return nil -}