diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml index 8e2f46d..f098b86 100644 --- a/.github/workflows/release-npm.yml +++ b/.github/workflows/release-npm.yml @@ -74,17 +74,17 @@ jobs: run: | VERSION=${{ github.event.release.tag_name || github.event.inputs.version }} FILE="dist/studio-mcp-server-${VERSION}-${{ matrix.target }}.${{ matrix.archive }}" - + # Check if release exists, create if not if ! gh release view "${VERSION}" >/dev/null 2>&1; then gh release create "${VERSION}" --title "Release ${VERSION}" --notes "Auto-generated release for ${VERSION}" fi - + # Upload or update asset if gh release view "${VERSION}" --json assets --jq '.assets[].name' | grep -q "studio-mcp-server-${VERSION}-${{ matrix.target }}.${{ matrix.archive }}"; then gh release delete-asset "${VERSION}" "studio-mcp-server-${VERSION}-${{ matrix.target }}.${{ matrix.archive }}" --yes fi - + gh release upload "${VERSION}" "${FILE}" publish-npm: @@ -107,10 +107,10 @@ jobs: VERSION=${{ github.event.release.tag_name || github.event.inputs.version }} # Remove 'v' prefix if present VERSION=${VERSION#v} - + # Update main package version npm version $VERSION --no-git-tag-version - + # Update optionalDependencies to use the same version jq --arg version "$VERSION" ' .optionalDependencies = { @@ -184,10 +184,10 @@ jobs: VERSION=${{ github.event.release.tag_name || github.event.inputs.version }} ARCHIVE_EXT=${{ matrix.target == 'x86_64-pc-windows-msvc' && 'zip' || 'tar.gz' }} ASSET_NAME="studio-mcp-server-${VERSION}-${{ matrix.target }}.${ARCHIVE_EXT}" - + # Download the release asset gh release download "${VERSION}" --pattern "${ASSET_NAME}" --dir ./temp - + # Extract the binary cd temp if [[ "${ARCHIVE_EXT}" == "zip" ]]; then @@ -195,7 +195,7 @@ jobs: else tar -xzf "${ASSET_NAME}" fi - + # Move binary to platform package directory mv "${{ matrix.binary }}" "../studio-mcp-server/platform-packages/${{ matrix.platform }}/" @@ -205,7 +205,7 @@ jobs: VERSION=${{ github.event.release.tag_name || github.event.inputs.version }} # Remove 'v' prefix if present VERSION=${VERSION#v} - + # Update platform package version jq --arg version "$VERSION" '.version = $version' package.json > package.json.tmp && mv package.json.tmp package.json @@ -216,4 +216,4 @@ jobs: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Clean up - run: rm -rf temp \ No newline at end of file + run: rm -rf temp diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index ff7417c..869174e 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -68,10 +68,10 @@ jobs: run: | # Create a temporary config for testing ./target/release/studio-mcp-server --init test-config.json - + # Test MCP Inspector can connect (run for 5 seconds then exit) timeout 5s npx --yes @modelcontextprotocol/inspector ./target/release/studio-mcp-server test-config.json --stdio || true - + # Clean up rm -f test-config.json shell: bash @@ -96,7 +96,7 @@ jobs: runs-on: ubuntu-latest needs: [test] if: always() - + steps: - name: Check test results run: | @@ -106,4 +106,4 @@ jobs: else echo "āŒ Tests failed on one or more platforms" exit 1 - fi \ No newline at end of file + fi diff --git a/.gitignore b/.gitignore index 825ec18..26b599c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,4 @@ packages/ # Test artifacts tmp/ -temp/ \ No newline at end of file +temp/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..de38518 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,46 @@ +# Pre-commit hooks for WindRiver Studio MCP Server +# Install: pip install pre-commit +# Setup: pre-commit install +# Run manually: pre-commit run --all-files + +repos: + # Rust-specific checks using local commands + - repo: local + hooks: + - id: cargo-fmt + name: cargo fmt + description: Format Rust code with rustfmt + entry: cargo fmt --all -- + language: system + types: [rust] + pass_filenames: false + + - id: cargo-clippy + name: cargo clippy + description: Lint Rust code with clippy + entry: cargo clippy --all-targets --all-features -- -D warnings + language: system + types: [rust] + pass_filenames: false + + - id: cargo-check + name: cargo check + description: Check Rust code compiles + entry: cargo check --all-targets --all-features + language: system + types: [rust] + pass_filenames: false + + # General file checks + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + exclude: '\.md$' + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-toml + - id: check-merge-conflict + - id: mixed-line-ending + args: ['--fix=lf'] diff --git a/Cargo.toml b/Cargo.toml index d045244..12111b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,13 @@ members = [ "studio-mcp-shared" ] +[workspace.package] +version = "0.4.0" +edition = "2024" +license = "MIT" +authors = ["PulseEngine Team"] +repository = "https://github.com/pulseengine/studio-mcp" + [workspace.dependencies] tokio = { version = "1.0", features = ["full"] } serde = { version = "1.0", features = ["derive"] } @@ -31,13 +38,13 @@ base64 = "0.22" regex = "1.0" # MCP dependencies - using PulseEngine MCP implementation -pulseengine-mcp-server = "0.7.0" -pulseengine-mcp-protocol = "0.7.0" -# pulseengine-mcp-macros = "0.7.0" # Available but requires significant refactoring +pulseengine-mcp-server = "0.11.0" +pulseengine-mcp-protocol = "0.11.0" +# pulseengine-mcp-macros = "0.11.0" # Available but requires significant refactoring async-trait = "0.1" [profile.release] opt-level = 3 lto = true codegen-units = 1 -panic = "abort" \ No newline at end of file +panic = "abort" diff --git a/README.md b/README.md index 5f29b44..ec4f35f 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,11 @@ # WindRiver Studio MCP Server -A production-ready Model Context Protocol (MCP) server providing AI assistants with secure access to WindRiver Studio CLI functionality, focusing on Pipeline Management (PLM) features. +[![GitHub Release](https://img.shields.io/github/v/release/pulseengine/studio-mcp?label=version)](https://github.com/pulseengine/studio-mcp/releases) +[![CI Status](https://img.shields.io/github/actions/workflow/status/pulseengine/studio-mcp/rust-ci.yml?branch=main&label=CI)](https://github.com/pulseengine/studio-mcp/actions) +[![npm version](https://img.shields.io/npm/v/@pulseengine/studio-mcp-server)](https://www.npmjs.com/package/@pulseengine/studio-mcp-server) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) -**Current Version: 0.2.15** - Built with PulseEngine MCP 0.7.0 +A production-ready Model Context Protocol (MCP) server providing AI assistants with secure access to WindRiver Studio CLI functionality, focusing on Pipeline Management (PLM) features. ## Features @@ -161,10 +164,8 @@ The server works with any MCP-compatible client: ## Development Status -**Current Release: v0.2.15** - Production-ready with PulseEngine MCP 0.7.0 - ### āœ… Completed Features -- **Core MCP Server**: Full PulseEngine MCP 0.7.0 integration +- **Core MCP Server**: Full PulseEngine MCP integration - **CLI Management**: Automatic download, version management, and execution - **Pipeline Management**: Complete PLM resource and tool providers - **Intelligent Caching**: Multi-layer caching with performance monitoring @@ -201,4 +202,4 @@ MIT License - see LICENSE file for details. - Built with the [PulseEngine MCP framework](https://github.com/pulseengine/mcp) - Designed for [WindRiver Studio](https://windriver.com) integration -- Implements the [Model Context Protocol](https://modelcontextprotocol.io/) \ No newline at end of file +- Implements the [Model Context Protocol](https://modelcontextprotocol.io/) diff --git a/docs/README.md b/docs/README.md index 4bf1c0b..c188862 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,4 +22,4 @@ This directory contains comprehensive documentation for the WindRiver Studio MCP - [GitHub Issues](https://github.com/pulseengine/studio-mcp/issues) - Bug reports and feature requests - [Discussions](https://github.com/pulseengine/studio-mcp/discussions) - Community support -- [Examples](../examples/) - Working code samples and integrations \ No newline at end of file +- [Examples](../examples/) - Working code samples and integrations diff --git a/docs/api.md b/docs/api.md index 06944dc..dd61ea5 100644 --- a/docs/api.md +++ b/docs/api.md @@ -361,4 +361,4 @@ Resources are cached based on data mutability: - **Semi-dynamic** (10 minutes): Pipeline lists, project information - **Dynamic** (1 minute): Active runs, live task status -Cache can be bypassed by including `"bypass_cache": true` in tool parameters. \ No newline at end of file +Cache can be bypassed by including `"bypass_cache": true` in tool parameters. diff --git a/docs/best-practices.md b/docs/best-practices.md index 097d0b2..5119fd4 100644 --- a/docs/best-practices.md +++ b/docs/best-practices.md @@ -467,4 +467,4 @@ result = client.call_tool("plm_list_pipelines", {"project": "MyProject"}) 7. **Disabling Caching for Performance** 8. **Using Shared Accounts Instead of Service Accounts** 9. **Not Setting Appropriate Timeouts** -10. **Ignoring Memory Limits** \ No newline at end of file +10. **Ignoring Memory Limits** diff --git a/docs/configuration.md b/docs/configuration.md index 064212b..8e671d4 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -249,4 +249,4 @@ See the [examples directory](../examples/) for complete configuration examples: - [Basic configuration](../examples/basic-config.json) - [Multi-instance setup](../examples/multi-instance-config.json) - [Development configuration](../examples/dev-config.json) -- [Production configuration](../examples/prod-config.json) \ No newline at end of file +- [Production configuration](../examples/prod-config.json) diff --git a/docs/installation.md b/docs/installation.md index b1b689d..ed9700a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -93,4 +93,4 @@ studio-mcp-server --health ## Troubleshooting -See the [troubleshooting guide](troubleshooting.md) for common installation issues. \ No newline at end of file +See the [troubleshooting guide](troubleshooting.md) for common installation issues. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index faad1b3..6c2d405 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -442,4 +442,4 @@ When reporting issues, include: - **Solution**: `xattr -d com.apple.quarantine /path/to/studio-mcp-server` 3. **Issue**: Linux GLIBC version compatibility - - **Solution**: Build from source or use npm package \ No newline at end of file + - **Solution**: Build from source or use npm package diff --git a/examples/README.md b/examples/README.md index d66a0f2..c40db83 100644 --- a/examples/README.md +++ b/examples/README.md @@ -68,4 +68,4 @@ Ask Claude to help with these typical Studio operations: - Check [troubleshooting guide](../docs/troubleshooting.md) for common issues - Review [configuration reference](../docs/configuration.md) for all options - Open [GitHub issues](https://github.com/pulseengine/studio-mcp/issues) for bugs -- Use [GitHub discussions](https://github.com/pulseengine/studio-mcp/discussions) for questions \ No newline at end of file +- Use [GitHub discussions](https://github.com/pulseengine/studio-mcp/discussions) for questions diff --git a/examples/basic-config.json b/examples/basic-config.json index 2cbde8a..adf6af8 100644 --- a/examples/basic-config.json +++ b/examples/basic-config.json @@ -11,4 +11,4 @@ "version": "auto", "auto_update": true } -} \ No newline at end of file +} diff --git a/examples/claude-desktop.md b/examples/claude-desktop.md index d15f0dc..a841e2a 100644 --- a/examples/claude-desktop.md +++ b/examples/claude-desktop.md @@ -266,4 +266,4 @@ For better performance with frequent queries: - Explore [workflow examples](pipeline-automation.md) - Set up [monitoring and alerts](monitoring-setup.md) - Review [best practices](../docs/best-practices.md) -- Check out [advanced integrations](vscode-integration.md) \ No newline at end of file +- Check out [advanced integrations](vscode-integration.md) diff --git a/examples/dev-config.json b/examples/dev-config.json index 85817b6..c5271e7 100644 --- a/examples/dev-config.json +++ b/examples/dev-config.json @@ -29,4 +29,4 @@ "log_level": "debug", "sensitive_data_filter": true } -} \ No newline at end of file +} diff --git a/examples/multi-instance-config.json b/examples/multi-instance-config.json index fd1fe40..e68e34d 100644 --- a/examples/multi-instance-config.json +++ b/examples/multi-instance-config.json @@ -42,4 +42,4 @@ "log_level": "info", "sensitive_data_filter": true } -} \ No newline at end of file +} diff --git a/examples/pipeline-automation.md b/examples/pipeline-automation.md index f45a01f..1e716f1 100644 --- a/examples/pipeline-automation.md +++ b/examples/pipeline-automation.md @@ -304,4 +304,4 @@ Set up Claude Desktop to work with Slack for team notifications: 1. **Use caching effectively** to reduce API calls 2. **Batch operations** when possible 3. **Implement rate limiting** to avoid overloading Studio -4. **Monitor resource usage** of automation scripts \ No newline at end of file +4. **Monitor resource usage** of automation scripts diff --git a/examples/prod-config.json b/examples/prod-config.json index 3f116ae..1f27e7a 100644 --- a/examples/prod-config.json +++ b/examples/prod-config.json @@ -32,4 +32,4 @@ "log_level": "info", "sensitive_data_filter": true } -} \ No newline at end of file +} diff --git a/studio-cli-manager/Cargo.toml b/studio-cli-manager/Cargo.toml index c196cce..e17e8e9 100644 --- a/studio-cli-manager/Cargo.toml +++ b/studio-cli-manager/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "studio-cli-manager" -version = "0.3.1" -edition = "2021" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true description = "Manager for downloading and updating WindRiver Studio CLI" -license = "MIT" -authors = ["PulseEngine Team"] -repository = "https://github.com/pulseengine/studio-mcp" [dependencies] tokio = { workspace = true } @@ -30,4 +30,4 @@ studio-mcp-shared = { path = "../studio-mcp-shared" } [dev-dependencies] tempfile = "3.8" mockito = "1.2" -tokio-test = "0.4" \ No newline at end of file +tokio-test = "0.4" diff --git a/studio-cli-manager/src/auth_cli.rs b/studio-cli-manager/src/auth_cli.rs index 7468715..c6bbbfc 100644 --- a/studio-cli-manager/src/auth_cli.rs +++ b/studio-cli-manager/src/auth_cli.rs @@ -65,10 +65,10 @@ impl AuthenticatedCliManager { // Check cache first { let cache = self.credentials_cache.read().await; - if let Some(credentials) = cache.get(&cache_key) { - if !credentials.needs_refresh() { - return Ok(credentials.clone()); - } + if let Some(credentials) = cache.get(&cache_key) + && !credentials.needs_refresh() + { + return Ok(credentials.clone()); } } diff --git a/studio-cli-manager/src/downloader.rs b/studio-cli-manager/src/downloader.rs index 639443f..7dbeb95 100644 --- a/studio-cli-manager/src/downloader.rs +++ b/studio-cli-manager/src/downloader.rs @@ -158,8 +158,8 @@ mod tests { #[test] fn test_gzip_decompression() { - use flate2::write::GzEncoder; use flate2::Compression; + use flate2::write::GzEncoder; let downloader = CliDownloader::new("https://example.com/cli".to_string()); let original_data = b"test data for compression"; diff --git a/studio-cli-manager/src/executor.rs b/studio-cli-manager/src/executor.rs index dedf6c3..88cec53 100644 --- a/studio-cli-manager/src/executor.rs +++ b/studio-cli-manager/src/executor.rs @@ -61,7 +61,7 @@ impl CliExecutor { return Err(StudioError::Cli(format!( "Command timed out after {} seconds", timeout_duration.as_secs() - ))) + ))); } }; diff --git a/studio-cli-manager/src/lib.rs b/studio-cli-manager/src/lib.rs index 0951aca..900dc91 100644 --- a/studio-cli-manager/src/lib.rs +++ b/studio-cli-manager/src/lib.rs @@ -126,26 +126,24 @@ impl CliManager { if let Some(pipeline_idx) = args .iter() .position(|&arg| arg == "--pipeline" || arg == "-p") + && let Some(pipeline_id) = args.get(pipeline_idx + 1) { - if let Some(pipeline_id) = args.get(pipeline_idx + 1) { - parameters.insert("pipeline_id".to_string(), pipeline_id.to_string()); - } + parameters.insert("pipeline_id".to_string(), pipeline_id.to_string()); } - if let Some(run_idx) = args.iter().position(|&arg| arg == "--run" || arg == "-r") { - if let Some(run_id) = args.get(run_idx + 1) { - parameters.insert("run_id".to_string(), run_id.to_string()); - } + if let Some(run_idx) = args.iter().position(|&arg| arg == "--run" || arg == "-r") + && let Some(run_id) = args.get(run_idx + 1) + { + parameters.insert("run_id".to_string(), run_id.to_string()); } // For commands like "plm pipeline create my-pipeline", extract the pipeline name if operation_parts.len() >= 3 && operation_parts[0] == "plm" && operation_parts[1] == "pipeline" + && let Some(pipeline_name) = operation_parts.get(3) { - if let Some(pipeline_name) = operation_parts.get(3) { - parameters.insert("pipeline_name".to_string(), pipeline_name.to_string()); - } + parameters.insert("pipeline_name".to_string(), pipeline_name.to_string()); } (operation, parameters) @@ -245,12 +243,12 @@ impl CliManager { if self.install_dir.exists() { for entry in std::fs::read_dir(&self.install_dir)? { let entry = entry?; - if entry.file_type()?.is_dir() { - if let Some(name) = entry.file_name().to_str() { - let cli_path = self.get_cli_path(name); - if cli_path.exists() { - versions.push(name.to_string()); - } + if entry.file_type()?.is_dir() + && let Some(name) = entry.file_name().to_str() + { + let cli_path = self.get_cli_path(name); + if cli_path.exists() { + versions.push(name.to_string()); } } } diff --git a/studio-cli-manager/src/version.rs b/studio-cli-manager/src/version.rs index c0b9e92..b07dd83 100644 --- a/studio-cli-manager/src/version.rs +++ b/studio-cli-manager/src/version.rs @@ -80,10 +80,10 @@ impl VersionManager { { let cache = self.cache.read().await; - if let Some((timestamp, versions)) = cache.as_ref() { - if timestamp.elapsed() < CACHE_DURATION { - return Ok(versions.clone()); - } + if let Some((timestamp, versions)) = cache.as_ref() + && timestamp.elapsed() < CACHE_DURATION + { + return Ok(versions.clone()); } } diff --git a/studio-mcp-server/Cargo.toml b/studio-mcp-server/Cargo.toml index d467f93..eff7fdc 100644 --- a/studio-mcp-server/Cargo.toml +++ b/studio-mcp-server/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "studio-mcp-server" -version = "0.3.1" -edition = "2021" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true description = "Model Context Protocol server for WindRiver Studio CLI" -license = "MIT" -authors = ["PulseEngine Team"] -repository = "https://github.com/pulseengine/studio-mcp" [dependencies] tokio = { workspace = true } @@ -42,4 +42,4 @@ development = ["tracing-subscriber/json"] [[bin]] name = "studio-mcp-server" -path = "src/main.rs" \ No newline at end of file +path = "src/main.rs" diff --git a/studio-mcp-server/npm/.npmignore b/studio-mcp-server/npm/.npmignore index b4db98c..c45c2ee 100644 --- a/studio-mcp-server/npm/.npmignore +++ b/studio-mcp-server/npm/.npmignore @@ -28,4 +28,4 @@ appveyor.yml # OS .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db diff --git a/studio-mcp-server/npm/README.md b/studio-mcp-server/npm/README.md index 35a1640..c88f23a 100644 --- a/studio-mcp-server/npm/README.md +++ b/studio-mcp-server/npm/README.md @@ -95,4 +95,4 @@ If installation fails: ## šŸ“„ License -MIT License - see the [LICENSE](https://github.com/pulseengine/studio-mcp/blob/main/LICENSE) file for details. \ No newline at end of file +MIT License - see the [LICENSE](https://github.com/pulseengine/studio-mcp/blob/main/LICENSE) file for details. diff --git a/studio-mcp-server/npm/index.js b/studio-mcp-server/npm/index.js index 298e671..2ee694b 100644 --- a/studio-mcp-server/npm/index.js +++ b/studio-mcp-server/npm/index.js @@ -12,7 +12,7 @@ function getBinaryName() { function getPlatformPackageName() { const platform = os.platform(); const arch = os.arch(); - + if (platform === "darwin") { return arch === "arm64" ? "@pulseengine/studio-mcp-server-darwin-arm64" : "@pulseengine/studio-mcp-server-darwin-x64"; } else if (platform === "linux") { @@ -20,7 +20,7 @@ function getPlatformPackageName() { } else if (platform === "win32") { return "@pulseengine/studio-mcp-server-win32-x64"; } - + throw new Error(`Unsupported platform: ${platform}-${arch}`); } @@ -28,7 +28,7 @@ function getBinaryPath() { try { const platformPackage = getPlatformPackageName(); const binaryName = getBinaryName(); - + // Try to find the platform-specific package try { const platformPackagePath = require.resolve(platformPackage); @@ -39,13 +39,13 @@ function getBinaryPath() { } catch (err) { // Platform package not found, continue to fallback } - + // Fallback to local bin directory (for GitHub releases fallback) const fallbackBinaryPath = path.join(__dirname, "bin", binaryName); if (existsSync(fallbackBinaryPath)) { return fallbackBinaryPath; } - + throw new Error(`Binary not found. Install with: npm install ${platformPackage}`); } catch (err) { throw new Error(`${err.message} @@ -69,16 +69,16 @@ if (require.main === module) { const { spawn } = require("child_process"); const binaryPath = getBinaryPath(); const [, , ...args] = process.argv; - + const child = spawn(binaryPath, args, { stdio: "inherit", cwd: process.cwd() }); - + child.on("close", (code) => { process.exit(code || 0); }); - + child.on("error", (err) => { console.error("Failed to run studio-mcp-server:", err.message); process.exit(1); @@ -87,4 +87,4 @@ if (require.main === module) { console.error(err.message); process.exit(1); } -} \ No newline at end of file +} diff --git a/studio-mcp-server/npm/install.js b/studio-mcp-server/npm/install.js index de9fee0..1deecfc 100644 --- a/studio-mcp-server/npm/install.js +++ b/studio-mcp-server/npm/install.js @@ -61,10 +61,10 @@ function getBinaryName() { function getDownloadUrl() { const version = require("./package.json").version; const platform = getPlatform(); - + // Determine archive format based on platform const archiveExtension = os.type() === "Windows_NT" ? "zip" : "tar.gz"; - + // Use GitHub releases for binary distribution return `https://github.com/pulseengine/studio-mcp/releases/download/v${version}/studio-mcp-server-v${version}-${platform}.${archiveExtension}`; } @@ -72,7 +72,7 @@ function getDownloadUrl() { function downloadFile(url, destination) { return new Promise((resolve, reject) => { const file = fs.createWriteStream(destination); - + https.get(url, (response) => { if (response.statusCode === 302 || response.statusCode === 301) { // Handle redirect @@ -80,19 +80,19 @@ function downloadFile(url, destination) { .then(resolve) .catch(reject); } - + if (response.statusCode !== 200) { reject(new Error(`HTTP ${response.statusCode}: ${response.statusMessage}`)); return; } - + response.pipe(file); - + file.on('finish', () => { file.close(); resolve(); }); - + file.on('error', (err) => { fs.unlink(destination, () => {}); reject(err); @@ -107,22 +107,22 @@ async function installBinary() { const binaryName = getBinaryName(); const downloadUrl = getDownloadUrl(); const platform = getPlatform(); - + console.log(`Platform detected: ${os.type()} ${os.arch()}`); console.log(`Target: ${platform}`); console.log(`Binary: ${binaryName}`); console.log(`Download URL: ${downloadUrl}`); - + // Create bin directory const binDir = path.join(__dirname, "bin"); if (!fs.existsSync(binDir)) { fs.mkdirSync(binDir, { recursive: true }); } - + // Download archive const archiveExtension = os.type() === "Windows_NT" ? "zip" : "tar.gz"; const archivePath = path.join(binDir, `studio-mcp-server.${archiveExtension}`); - + console.log("šŸ“„ Downloading binary..."); try { await downloadFile(downloadUrl, archivePath); @@ -130,7 +130,7 @@ async function installBinary() { } catch (err) { throw new Error(`Download failed: ${err.message}`); } - + // Extract archive console.log("šŸ“¦ Extracting binary..."); try { @@ -141,10 +141,10 @@ async function installBinary() { // Extract tar.gz (Unix) execSync(`cd "${binDir}" && tar -xzf "${path.basename(archivePath)}"`, { stdio: 'inherit' }); } - + // Clean up archive fs.unlinkSync(archivePath); - + // Make binary executable on Unix if (os.type() !== "Windows_NT") { const binaryPath = path.join(binDir, binaryName); @@ -152,7 +152,7 @@ async function installBinary() { fs.chmodSync(binaryPath, 0o755); } } - + console.log("āœ… Binary installed successfully"); } catch (err) { throw new Error(`Extraction failed: ${err.message}`); @@ -162,7 +162,7 @@ async function installBinary() { // Run installation installBinary().catch(err => { console.error("āŒ Failed to install studio-mcp-server binary:", err.message); - + // Provide helpful error message console.error("\nšŸ“‹ Installation failed. You can:"); console.error("1. Install Rust and build from source:"); @@ -172,6 +172,6 @@ installBinary().catch(err => { console.error(""); console.error("2. Download binary manually from:"); console.error(" https://github.com/pulseengine/studio-mcp/releases"); - + process.exit(1); -}); \ No newline at end of file +}); diff --git a/studio-mcp-server/npm/package.json b/studio-mcp-server/npm/package.json index baef371..4995250 100644 --- a/studio-mcp-server/npm/package.json +++ b/studio-mcp-server/npm/package.json @@ -47,4 +47,4 @@ "x64", "arm64" ] -} \ No newline at end of file +} diff --git a/studio-mcp-server/npm/run.js b/studio-mcp-server/npm/run.js index 8126c5f..f622ad2 100644 --- a/studio-mcp-server/npm/run.js +++ b/studio-mcp-server/npm/run.js @@ -14,9 +14,9 @@ try { // Run the binary directly with all arguments passed through const [, , ...args] = process.argv; -const result = spawnSync(binaryPath, args, { - cwd: process.cwd(), - stdio: "inherit" +const result = spawnSync(binaryPath, args, { + cwd: process.cwd(), + stdio: "inherit" }); if (result.error) { @@ -25,4 +25,4 @@ if (result.error) { process.exit(1); } -process.exit(result.status || 0); \ No newline at end of file +process.exit(result.status || 0); diff --git a/studio-mcp-server/npm/uninstall.js b/studio-mcp-server/npm/uninstall.js index bf27b94..59785f0 100644 --- a/studio-mcp-server/npm/uninstall.js +++ b/studio-mcp-server/npm/uninstall.js @@ -13,4 +13,4 @@ const binary = new Binary(getBinaryName(), ""); binary.uninstall().catch(err => { // Don't fail uninstall if binary cleanup fails console.warn("Warning: Failed to clean up binary:", err.message); -}); \ No newline at end of file +}); diff --git a/studio-mcp-server/platform-packages/darwin-arm64/package.json b/studio-mcp-server/platform-packages/darwin-arm64/package.json index 62b9b22..7578f2f 100644 --- a/studio-mcp-server/platform-packages/darwin-arm64/package.json +++ b/studio-mcp-server/platform-packages/darwin-arm64/package.json @@ -15,4 +15,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/studio-mcp-server/platform-packages/darwin-x64/package.json b/studio-mcp-server/platform-packages/darwin-x64/package.json index 81eea3f..91a9148 100644 --- a/studio-mcp-server/platform-packages/darwin-x64/package.json +++ b/studio-mcp-server/platform-packages/darwin-x64/package.json @@ -15,4 +15,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/studio-mcp-server/platform-packages/linux-x64/package.json b/studio-mcp-server/platform-packages/linux-x64/package.json index dce995c..b423d80 100644 --- a/studio-mcp-server/platform-packages/linux-x64/package.json +++ b/studio-mcp-server/platform-packages/linux-x64/package.json @@ -15,4 +15,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/studio-mcp-server/platform-packages/win32-x64/package.json b/studio-mcp-server/platform-packages/win32-x64/package.json index b1fb5ab..9b2590a 100644 --- a/studio-mcp-server/platform-packages/win32-x64/package.json +++ b/studio-mcp-server/platform-packages/win32-x64/package.json @@ -15,4 +15,4 @@ "publishConfig": { "access": "public" } -} \ No newline at end of file +} diff --git a/studio-mcp-server/src/cache/invalidation_service.rs b/studio-mcp-server/src/cache/invalidation_service.rs index e2f324a..d3d6758 100644 --- a/studio-mcp-server/src/cache/invalidation_service.rs +++ b/studio-mcp-server/src/cache/invalidation_service.rs @@ -493,9 +493,11 @@ mod tests { // Should match create pattern and invalidate relevant caches assert!(!result.matched_patterns.is_empty()); - assert!(result - .matched_patterns - .contains(&"plm.pipeline.create".to_string())); + assert!( + result + .matched_patterns + .contains(&"plm.pipeline.create".to_string()) + ); // Test run start operation parameters.clear(); @@ -508,8 +510,10 @@ mod tests { // Should match run start pattern assert!(!result.matched_patterns.is_empty()); - assert!(result - .matched_patterns - .contains(&"plm.run.start".to_string())); + assert!( + result + .matched_patterns + .contains(&"plm.run.start".to_string()) + ); } } diff --git a/studio-mcp-server/src/cache/mod.rs b/studio-mcp-server/src/cache/mod.rs index bf2b44b..d256b38 100644 --- a/studio-mcp-server/src/cache/mod.rs +++ b/studio-mcp-server/src/cache/mod.rs @@ -444,13 +444,14 @@ impl CacheStats { } // Track hottest keys (most accessed) - if let Some(key) = key { - if hit && !perf.hottest_keys.contains(&key.to_string()) { - perf.hottest_keys.push(key.to_string()); - // Keep only top 10 hottest keys - if perf.hottest_keys.len() > 10 { - perf.hottest_keys.remove(0); - } + if let Some(key) = key + && hit + && !perf.hottest_keys.contains(&key.to_string()) + { + perf.hottest_keys.push(key.to_string()); + // Keep only top 10 hottest keys + if perf.hottest_keys.len() > 10 { + perf.hottest_keys.remove(0); } } } diff --git a/studio-mcp-server/src/cache/plm_cache.rs b/studio-mcp-server/src/cache/plm_cache.rs index 628774b..b0f1a16 100644 --- a/studio-mcp-server/src/cache/plm_cache.rs +++ b/studio-mcp-server/src/cache/plm_cache.rs @@ -103,8 +103,7 @@ impl PlmCache { ); trace!( "Cache miss for PLM key: {} ({}ms)", - full_key, - access_time_ms + full_key, access_time_ms ); } } @@ -1184,10 +1183,12 @@ mod tests { // Verify items exist assert!(cache.get(&context, "pipeline:def:test").await.is_some()); - assert!(cache - .get(&context, "run:details:123:completed") - .await - .is_some()); + assert!( + cache + .get(&context, "run:details:123:completed") + .await + .is_some() + ); assert!(cache.get(&context, "pipelines:list").await.is_some()); assert!(cache.get(&context, "run:events:456").await.is_some()); @@ -1244,10 +1245,12 @@ mod tests { json!({"env": "prod"}), ) .await; - assert!(prod_cache - .get(&context, "pipeline:def:prod") - .await - .is_some()); + assert!( + prod_cache + .get(&context, "pipeline:def:prod") + .await + .is_some() + ); // Test testing configuration let test_cache = PlmCache::with_config(CacheConfig::testing()); @@ -1264,10 +1267,12 @@ mod tests { json!({"env": "test"}), ) .await; - assert!(test_cache - .get(&context, "pipeline:def:test") - .await - .is_some()); + assert!( + test_cache + .get(&context, "pipeline:def:test") + .await + .is_some() + ); // Verify different TTL values let dev_immutable_ttl = dev_cache.config.get_ttl(CacheType::Immutable); diff --git a/studio-mcp-server/src/main.rs b/studio-mcp-server/src/main.rs index a47fb21..c23f52d 100644 --- a/studio-mcp-server/src/main.rs +++ b/studio-mcp-server/src/main.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use std::env; use tracing::{error, info}; -use tracing_subscriber::{fmt, EnvFilter}; +use tracing_subscriber::{EnvFilter, fmt}; mod auth_middleware; mod cache; diff --git a/studio-mcp-server/src/resources/plm.rs b/studio-mcp-server/src/resources/plm.rs index 834a231..13d62a2 100644 --- a/studio-mcp-server/src/resources/plm.rs +++ b/studio-mcp-server/src/resources/plm.rs @@ -79,20 +79,18 @@ impl PlmResourceProvider { if let Some(pipeline_idx) = args_vec .iter() .position(|arg| arg == "--pipeline" || arg == "-p") + && let Some(pipeline_id) = args_vec.get(pipeline_idx + 1) { - if let Some(pipeline_id) = args_vec.get(pipeline_idx + 1) { - parameters.insert("pipeline_id".to_string(), pipeline_id.clone()); - } + parameters.insert("pipeline_id".to_string(), pipeline_id.clone()); } // Extract run ID if let Some(run_idx) = args_vec .iter() .position(|arg| arg == "--run" || arg == "-r") + && let Some(run_id) = args_vec.get(run_idx + 1) { - if let Some(run_id) = args_vec.get(run_idx + 1) { - parameters.insert("run_id".to_string(), run_id.clone()); - } + parameters.insert("run_id".to_string(), run_id.clone()); } // Extract entity name from positional arguments (e.g., "plm pipeline create my-pipeline") @@ -540,14 +538,14 @@ impl PlmResourceProvider { let cache_key = PlmCache::pipeline_list_key(); // Try cache first - if let Some(cached_value) = self.cache.get(&context, &cache_key).await { - if let Some(pipelines) = cached_value.get("pipelines").and_then(|v| v.as_array()) { - debug!( - "Returning cached pipeline list ({} pipelines)", - pipelines.len() - ); - return Ok(pipelines.clone()); - } + if let Some(cached_value) = self.cache.get(&context, &cache_key).await + && let Some(pipelines) = cached_value.get("pipelines").and_then(|v| v.as_array()) + { + debug!( + "Returning cached pipeline list ({} pipelines)", + pipelines.len() + ); + return Ok(pipelines.clone()); } // Cache miss - fetch from CLI diff --git a/studio-mcp-server/src/tools/plm.rs b/studio-mcp-server/src/tools/plm.rs index 088bee2..06945db 100644 --- a/studio-mcp-server/src/tools/plm.rs +++ b/studio-mcp-server/src/tools/plm.rs @@ -1,7 +1,7 @@ //! PLM (Pipeline Management) tool provider use pulseengine_mcp_protocol::{Content, Tool}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::sync::Arc; use std::time::Duration; use studio_cli_manager::CliManager; @@ -474,7 +474,7 @@ impl PlmToolProvider { "description": "Show logs since timestamp (ISO format)" }, "query_since": { - "type": "string", + "type": "string", "description": "Query logs since timestamp (more precise than since)" }, "query_until": { @@ -748,7 +748,7 @@ impl PlmToolProvider { "description": "Task definition in YAML or JSON format" }, "definition_file": { - "type": "string", + "type": "string", "description": "Path to YAML/JSON file containing task definition" }, "name": { @@ -1629,28 +1629,28 @@ impl PlmToolProvider { let mut cli_args = vec!["plm", "run", "get", &run_id, "--output", "json"]; // Add additional options based on parameters - if let Some(run_config) = args.get("run_config").and_then(|v| v.as_bool()) { - if run_config { - cli_args.push("--run-config"); - } + if let Some(run_config) = args.get("run_config").and_then(|v| v.as_bool()) + && run_config + { + cli_args.push("--run-config"); } - if let Some(detailed_info) = args.get("detailed_info").and_then(|v| v.as_bool()) { - if detailed_info { - cli_args.push("--detailed-info"); - } + if let Some(detailed_info) = args.get("detailed_info").and_then(|v| v.as_bool()) + && detailed_info + { + cli_args.push("--detailed-info"); } - if let Some(include_tasks) = args.get("include_tasks").and_then(|v| v.as_bool()) { - if include_tasks { - cli_args.push("--include-tasks"); - } + if let Some(include_tasks) = args.get("include_tasks").and_then(|v| v.as_bool()) + && include_tasks + { + cli_args.push("--include-tasks"); } - if let Some(execution_logs) = args.get("execution_logs").and_then(|v| v.as_bool()) { - if execution_logs { - cli_args.push("--execution-logs"); - } + if let Some(execution_logs) = args.get("execution_logs").and_then(|v| v.as_bool()) + && execution_logs + { + cli_args.push("--execution-logs"); } match self.cli_manager.execute(&cli_args, None).await { @@ -2617,12 +2617,16 @@ impl PlmToolProvider { let old_param_name = args .get("old_param_name") .and_then(|v| v.as_str()) - .ok_or_else(|| StudioError::InvalidOperation("old_param_name is required".to_string()))?; + .ok_or_else(|| { + StudioError::InvalidOperation("old_param_name is required".to_string()) + })?; let new_param_name = args .get("new_param_name") .and_then(|v| v.as_str()) - .ok_or_else(|| StudioError::InvalidOperation("new_param_name is required".to_string()))?; + .ok_or_else(|| { + StudioError::InvalidOperation("new_param_name is required".to_string()) + })?; cli_args.extend_from_slice(&["--old-param-name", old_param_name]); cli_args.extend_from_slice(&["--new-param-name", new_param_name]); @@ -2694,7 +2698,10 @@ impl PlmToolProvider { } // Handle create_ssh flag (default is true) - let create_ssh = args.get("create_ssh").and_then(|v| v.as_bool()).unwrap_or(true); + let create_ssh = args + .get("create_ssh") + .and_then(|v| v.as_bool()) + .unwrap_or(true); if !create_ssh { cli_args.push("--create-ssh=false"); } diff --git a/studio-mcp-server/tests/integration_tests.rs b/studio-mcp-server/tests/integration_tests.rs index 3d89623..d7c0f9b 100644 --- a/studio-mcp-server/tests/integration_tests.rs +++ b/studio-mcp-server/tests/integration_tests.rs @@ -157,6 +157,8 @@ async fn test_windriver_studio_mcp_integration() { "quick_operations": 5, "medium_operations": 30, "long_operations": 300, + "pipeline_start": 1800, + "pipeline_follow": 3600, "network_requests": 60 }}, "auto_update": false, diff --git a/studio-mcp-server/tests/mock_plm_server.rs b/studio-mcp-server/tests/mock_plm_server.rs index d273b01..a080edc 100644 --- a/studio-mcp-server/tests/mock_plm_server.rs +++ b/studio-mcp-server/tests/mock_plm_server.rs @@ -5,12 +5,12 @@ //! error scenarios, and resource management. use chrono::{DateTime, Duration, Utc}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::collections::HashMap; use tokio::sync::RwLock; use wiremock::{ - matchers::{method, path, path_regex, query_param}, Mock, MockServer, ResponseTemplate, + matchers::{method, path, path_regex, query_param}, }; /// Comprehensive PLM mock server @@ -1876,9 +1876,11 @@ mod tests { assert!(resources["data"]["cpu"]["total_cores"].as_u64().unwrap() > 0); assert!(resources["data"]["memory"]["total_gb"].as_u64().unwrap() > 0); // active_builds is u64, so it's always >= 0 - just verify it exists - assert!(resources["data"]["builds"]["active_builds"] - .as_u64() - .is_some()); + assert!( + resources["data"]["builds"]["active_builds"] + .as_u64() + .is_some() + ); } #[tokio::test] diff --git a/studio-mcp-server/tests/mock_studio_server.rs b/studio-mcp-server/tests/mock_studio_server.rs index 9535724..b2b2aa8 100644 --- a/studio-mcp-server/tests/mock_studio_server.rs +++ b/studio-mcp-server/tests/mock_studio_server.rs @@ -6,12 +6,12 @@ //! - Versioned REST API endpoints (/api/v1/ through /api/v5/) //! - JSON-RPC 2.0 message format compliance -use serde_json::{json, Value}; +use serde_json::{Value, json}; use std::collections::HashMap; use tokio::sync::RwLock; use wiremock::{ - matchers::{header, method, path, path_regex}, Mock, MockServer, ResponseTemplate, + matchers::{header, method, path, path_regex}, }; /// Mock WindRiver Studio server with complete protocol simulation diff --git a/studio-mcp-shared/Cargo.toml b/studio-mcp-shared/Cargo.toml index d7fd5f2..169e061 100644 --- a/studio-mcp-shared/Cargo.toml +++ b/studio-mcp-shared/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "studio-mcp-shared" -version = "0.3.1" -edition = "2021" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true description = "Shared types and utilities for WindRiver Studio MCP server" -license = "MIT" -authors = ["PulseEngine Team"] -repository = "https://github.com/pulseengine/studio-mcp" [dependencies] serde = { workspace = true } @@ -24,4 +24,4 @@ base64 = { workspace = true } sha1 = { workspace = true } hex = { workspace = true } tokio = { workspace = true } -tracing = { workspace = true } \ No newline at end of file +tracing = { workspace = true } diff --git a/studio-mcp-shared/src/auth.rs b/studio-mcp-shared/src/auth.rs index f5674a1..c1274ba 100644 --- a/studio-mcp-shared/src/auth.rs +++ b/studio-mcp-shared/src/auth.rs @@ -2,10 +2,10 @@ use crate::{Result, StudioError}; use aes_gcm::{AeadInPlace, Aes256Gcm, KeyInit, Nonce}; -use base64::{engine::general_purpose, Engine as _}; +use base64::{Engine as _, engine::general_purpose}; use chrono::{DateTime, Duration, Utc}; use keyring::Entry; -use rand::{rngs::OsRng, RngCore}; +use rand::{RngCore, rngs::OsRng}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -404,28 +404,28 @@ impl AuthManager { ) -> Result { let mut credentials = self.get_credentials(instance_id, environment)?; - if let Some(token) = &credentials.token { - if let Some(refresh_token) = &token.refresh_token { - // Make refresh request to Studio API - // For now, create a new mock token - let new_token = AuthToken::new( - "refreshed_access_token".to_string(), - Some(refresh_token.clone()), - 3600, - token.studio_url.clone(), - token.scopes.clone(), - ); - - // Update stored credentials - credentials.set_token(new_token.clone()); - self.storage.store_credentials(&credentials)?; - - // Update cache - let cache_key = format!("{environment}:{instance_id}"); - self.credentials_cache.insert(cache_key, credentials); - - return Ok(new_token); - } + if let Some(token) = &credentials.token + && let Some(refresh_token) = &token.refresh_token + { + // Make refresh request to Studio API + // For now, create a new mock token + let new_token = AuthToken::new( + "refreshed_access_token".to_string(), + Some(refresh_token.clone()), + 3600, + token.studio_url.clone(), + token.scopes.clone(), + ); + + // Update stored credentials + credentials.set_token(new_token.clone()); + self.storage.store_credentials(&credentials)?; + + // Update cache + let cache_key = format!("{environment}:{instance_id}"); + self.credentials_cache.insert(cache_key, credentials); + + return Ok(new_token); } Err(StudioError::Auth("No refresh token available".to_string())) diff --git a/studio-mcp-shared/src/auth_service.rs b/studio-mcp-shared/src/auth_service.rs index ac30a5f..4a65f95 100644 --- a/studio-mcp-shared/src/auth_service.rs +++ b/studio-mcp-shared/src/auth_service.rs @@ -1,7 +1,7 @@ //! Authentication service that integrates with WindRiver Studio CLI use crate::{AuthCredentials, AuthManager, AuthToken, Result, StudioError, TokenStorage}; -use jsonwebtoken::{decode_header, Algorithm, DecodingKey, Validation}; +use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode_header}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::time::Duration; @@ -169,24 +169,24 @@ impl StudioAuthService { &mut self, mut credentials: AuthCredentials, ) -> Result { - if let Some(token) = &credentials.token { - if let Some(refresh_token) = &token.refresh_token { - // Attempt token refresh - match self - .refresh_token_with_api(&credentials.studio_url, refresh_token) - .await - { - Ok(new_token) => { - credentials.set_token(new_token); - self.auth_manager.store_credentials(&credentials).await?; - return Ok(credentials); - } - Err(e) => { - // If refresh fails, credentials are invalid - self.logout(&credentials.instance_id, &credentials.environment) - .await?; - return Err(StudioError::Auth(format!("Token refresh failed: {e}"))); - } + if let Some(token) = &credentials.token + && let Some(refresh_token) = &token.refresh_token + { + // Attempt token refresh + match self + .refresh_token_with_api(&credentials.studio_url, refresh_token) + .await + { + Ok(new_token) => { + credentials.set_token(new_token); + self.auth_manager.store_credentials(&credentials).await?; + return Ok(credentials); + } + Err(e) => { + // If refresh fails, credentials are invalid + self.logout(&credentials.instance_id, &credentials.environment) + .await?; + return Err(StudioError::Auth(format!("Token refresh failed: {e}"))); } } } @@ -199,13 +199,13 @@ impl StudioAuthService { /// Logout and remove stored credentials pub async fn logout(&mut self, instance_id: &str, environment: &str) -> Result<()> { // Get credentials to notify server - if let Ok(credentials) = self.auth_manager.get_credentials(instance_id, environment) { - if let Ok(token) = credentials.get_valid_token() { - // Attempt to revoke token on server (best effort) - let _ = self - .revoke_token(&credentials.studio_url, &token.access_token) - .await; - } + if let Ok(credentials) = self.auth_manager.get_credentials(instance_id, environment) + && let Ok(token) = credentials.get_valid_token() + { + // Attempt to revoke token on server (best effort) + let _ = self + .revoke_token(&credentials.studio_url, &token.access_token) + .await; } // Remove from local storage diff --git a/studio-mcp-shared/src/token_validator.rs b/studio-mcp-shared/src/token_validator.rs index be42625..fddb6c8 100644 --- a/studio-mcp-shared/src/token_validator.rs +++ b/studio-mcp-shared/src/token_validator.rs @@ -2,7 +2,7 @@ use crate::{AuthToken, Result, StudioError}; use chrono::{DateTime, Duration, Utc}; -use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, TokenData, Validation}; +use jsonwebtoken::{Algorithm, DecodingKey, TokenData, Validation, decode, decode_header}; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -242,18 +242,18 @@ impl TokenValidator { // Check cache first { let cache = self.jwks_cache.read().await; - if let Some(entry) = cache.get(studio_url) { - if entry.expires_at > Utc::now() { - // Try to find key by kid (key ID) - if let Some(kid) = &header.kid { - if let Some(key) = entry.keys.get(kid) { - return Ok(key.clone()); - } - } - // Fallback to first available key - if let Some(key) = entry.keys.values().next() { - return Ok(key.clone()); - } + if let Some(entry) = cache.get(studio_url) + && entry.expires_at > Utc::now() + { + // Try to find key by kid (key ID) + if let Some(kid) = &header.kid + && let Some(key) = entry.keys.get(kid) + { + return Ok(key.clone()); + } + // Fallback to first available key + if let Some(key) = entry.keys.values().next() { + return Ok(key.clone()); } } } @@ -483,7 +483,9 @@ mod tests { // Test valid permissions assert!(validator.validate_permissions(&claims, &["read".to_string()])); - assert!(validator.validate_permissions(&claims, &["read".to_string(), "write".to_string()])); + assert!( + validator.validate_permissions(&claims, &["read".to_string(), "write".to_string()]) + ); // Test invalid permissions assert!(!validator.validate_permissions(&claims, &["super-admin".to_string()]));