You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iggy.apache.org by pi...@apache.org on 2025/07/24 07:07:05 UTC
(iggy) branch master updated: feat(mcp): add Model Context Protocol (MCP) server (#2030)
This is an automated email from the ASF dual-hosted git repository.
piotr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/iggy.git
The following commit(s) were added to refs/heads/master by this push:
new 6ffa79b7 feat(mcp): add Model Context Protocol (MCP) server (#2030)
6ffa79b7 is described below
commit 6ffa79b78e0563b32177f46981d2f880655e049f
Author: Piotr Gankiewicz <pi...@gmail.com>
AuthorDate: Thu Jul 24 09:07:02 2025 +0200
feat(mcp): add Model Context Protocol (MCP) server (#2030)
Implement Model Context Protocol (MCP) server for Iggy using `rmcp` SDK
from Anthropic. Additionally upgrade and update all the dependencies.
---
.convco | 2 +-
.dockerignore | 9 +-
.github/changed-files-config.json | 4 +-
.github/workflows/ci-prod-rust.yml | 6 +-
.github/workflows/publish_connectors.yml | 211 +++++-
.../{publish_server.yml => publish_mcp.yml} | 65 +-
.github/workflows/publish_server.yml | 10 +-
.github/workflows/release_bench.yml | 16 +-
.github/workflows/release_connectors.yml | 16 +-
.../{release_bench.yml => release_mcp.yml} | 36 +-
Cargo.lock | 682 ++++++++++++------
Cargo.toml | 35 +-
DEPENDENCIES.md | 123 ++--
Dockerfile.ci.dockerignore | 3 +
docker-compose.yml => Dockerfile.connectors.ci | 32 +-
...ignore => Dockerfile.connectors.ci.dockerignore | 3 +
Dockerfile.ci.dockerignore => Dockerfile.mcp.ci | 25 +-
....dockerignore => Dockerfile.mcp.ci.dockerignore | 3 +
core/ai/README.md | 5 +
core/ai/mcp/Cargo.toml | 25 +
docker-compose.yml => core/ai/mcp/Dockerfile | 33 +-
core/ai/mcp/README.md | 62 ++
core/ai/mcp/config.toml | 49 ++
.../runtime/src/api/mod.rs => ai/mcp/src/api.rs} | 94 ++-
core/ai/mcp/src/configs.rs | 178 +++++
.../src/certificates.rs => ai/mcp/src/error.rs} | 30 +-
core/ai/mcp/src/main.rs | 198 ++++++
core/ai/mcp/src/service/mod.rs | 773 +++++++++++++++++++++
core/ai/mcp/src/service/requests.rs | 559 +++++++++++++++
core/ai/mcp/src/stream.rs | 62 ++
core/bench/dashboard/server/Cargo.toml | 4 +-
core/cli/Cargo.toml | 2 +-
core/common/Cargo.toml | 2 +-
core/common/src/certificates.rs | 2 +-
.../connectors/runtime/Dockerfile | 33 +-
core/connectors/runtime/src/api/mod.rs | 4 +-
core/connectors/sdk/Cargo.toml | 10 +-
core/integration/Cargo.toml | 9 +-
core/integration/src/lib.rs | 2 +
core/integration/src/test_mcp_server.rs | 232 +++++++
core/integration/src/test_tls_utils.rs | 2 +-
core/integration/tests/mcp/mod.rs | 701 +++++++++++++++++++
core/integration/tests/mod.rs | 1 +
docker-compose.yml | 2 -
44 files changed, 3823 insertions(+), 532 deletions(-)
diff --git a/.convco b/.convco
index 507d7885..2f7fc2cc 100644
--- a/.convco
+++ b/.convco
@@ -1 +1 @@
-scopeRegex: "(ci|sdk|server|connectors|cli|bench|misc|docs|io_uring|test|repo|deps|deps-dev)(,(ci|sdk|server|connectors|cli|bench|misc|docs|io_uring|test|repo|deps|deps-dev))*"
+scopeRegex: "(ci|sdk|server|connectors|cli|bench|misc|mcp|docs|io_uring|test|repo|deps|deps-dev)(,(ci|sdk|server|connectors|cli|bench|misc|mcp|docs|io_uring|test|repo|deps|deps-dev))*"
diff --git a/.dockerignore b/.dockerignore
index 0caab147..336912aa 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -3,8 +3,15 @@
.git
.github
.gitignore
+.idea
+.vscode
+.zed
/assets
-/certs
+/core/certs
+/helm
+/licenses
/local_data
+/performance_results
/scripts
/target
+/web
diff --git a/.github/changed-files-config.json b/.github/changed-files-config.json
index a96ea4e2..0588b7a0 100644
--- a/.github/changed-files-config.json
+++ b/.github/changed-files-config.json
@@ -1,6 +1,6 @@
{
"rust": [
- "core/(bench|cli|examples|integration|sdk|server|tools|connectors).*\\.rs",
+ "core/(bench|cli|examples|integration|sdk|server|tools|connectors|ai).*\\.rs",
"bdd/rust/.*\\.rs",
"core/Cargo.toml",
"Cargo.toml",
@@ -15,7 +15,7 @@
".github/workflows/ci-check-pr.yml"
],
"rust-prod": [
- "core/(bench|cli|examples|integration|sdk|server|tools|connectors).*\\.rs",
+ "core/(bench|cli|examples|integration|sdk|server|tools|connectors|ai).*\\.rs",
"bdd/rust/.*\\.rs",
"core/Cargo.toml",
"Cargo.toml",
diff --git a/.github/workflows/ci-prod-rust.yml b/.github/workflows/ci-prod-rust.yml
index 7efec3e1..f7672d0f 100644
--- a/.github/workflows/ci-prod-rust.yml
+++ b/.github/workflows/ci-prod-rust.yml
@@ -75,9 +75,9 @@ jobs:
echo -n "test" | gnome-keyring-daemon --unlock
- name: Prepare ${{ matrix.platform.target }} toolchain
run: |
- rustup toolchain add --profile=minimal stable
- rustup target add --toolchain=stable ${{ matrix.platform.target }}
- rustup override set stable
+ rustup toolchain add --profile=minimal stable
+ rustup target add --toolchain=stable ${{ matrix.platform.target }}
+ rustup override set stable
- name: Install cross tool
uses: taiki-e/install-action@v2
with:
diff --git a/.github/workflows/publish_connectors.yml b/.github/workflows/publish_connectors.yml
index e70ff128..9f269a7e 100644
--- a/.github/workflows/publish_connectors.yml
+++ b/.github/workflows/publish_connectors.yml
@@ -16,18 +16,23 @@
# under the License.
name: publish_connectors
+
on:
push:
tags:
- - "iggy-connectors-*"
+ - "connectors-*"
env:
+ DOCKERHUB_REGISTRY_NAME: apache/iggy-connect
+ CRATE_NAME: iggy-connectors
GITHUB_TOKEN: ${{ github.token }}
+ RUST_BACKTRACE: 1
CARGO_TERM_COLOR: always
+ IGGY_CI_BUILD: true
jobs:
validate:
- if: startsWith(github.ref, 'refs/tags/iggy-connectors-')
+ if: startsWith(github.ref, 'refs/tags/connectors-')
runs-on: ubuntu-latest
steps:
- name: Extract tag name
@@ -37,8 +42,8 @@ jobs:
- name: Validate tag format
run: |
TAG=${TAG}
- if [[ ! "$TAG" =~ ^iggy-connectors-([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
- echo "Tag $TAG does not match strict semver format (iggy-connectors-X.Y.Z where 0 <= X,Y,Z <= 999)"
+ if [[ ! "$TAG" =~ ^connectors-([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
+ echo "Tag $TAG does not match strict semver format (connectors-X.Y.Z where 0 <= X,Y,Z <= 999)"
exit 1
fi
echo "Valid tag: $TAG"
@@ -62,14 +67,14 @@ jobs:
- name: Extract iggy-connectors version from Cargo.toml
id: extract_version
run: |
- version=$(cargo pkgid -p iggy-connectors | cut -d@ -f2)
- echo "iggy_connectors_version=$version" >> "$GITHUB_OUTPUT"
+ version=$(cargo pkgid -p iggy-connectors | cut -d# -f2 | cut -d: -f2)
+ echo "connectors_version=$version" >> "$GITHUB_OUTPUT"
echo "::notice ::Version from Cargo.toml $version"
- name: Check if version from Cargo.toml is the same as the tag
id: check_git_tag
run: |
- if [[ "iggy-connectors-${{ steps.extract_version.outputs.iggy_connectors_version }}" == "${{ steps.extract_tag.outputs.tag_name }}" ]];
+ if [[ "connectors-${{ steps.extract_version.outputs.connectors_version }}" == "${{ steps.extract_tag.outputs.tag_name }}" ]];
then
echo "::notice ::Tag ${{ steps.extract_tag.outputs.tag_name }} matches the version in Cargo.toml"
echo "tag_matches=true" >> "$GITHUB_OUTPUT"
@@ -79,41 +84,203 @@ jobs:
fi
outputs:
- iggy_connectors_version: ${{ steps.extract_tag.outputs.tag_name }}
+ connectors_version: ${{ steps.extract_version.outputs.connectors_version }}
tag_created: ${{ steps.check_git_tag.outputs.tag_matches }}
- publish:
- name: Publish Connectors Runtime on crates.io
+ release_and_publish:
+ name: build release ${{ matrix.platform.os_name }}
needs: tag
- if: ${{ needs.tag.outputs.tag_created == 'true' }}
- runs-on: ubuntu-latest
+ runs-on: ${{ matrix.platform.os }}
+ strategy:
+ matrix:
+ platform:
+ - os_name: Linux-x86_64-musl
+ os: ubuntu-latest
+ target: x86_64-unknown-linux-musl
+ bin:
+ - iggy-connectors
+ name: iggy-connectors-Linux-x86_64-musl.tar.gz
+ cargo_command: cargo
+ docker_arch: linux/amd64
+ cross: false
+
+ - os_name: Linux-aarch64-musl
+ os: ubuntu-latest
+ target: aarch64-unknown-linux-musl
+ bin:
+ - iggy-connectors
+ name: iggy-connectors-Linux-aarch64-musl.tar.gz
+ docker_arch: linux/arm64/v8
+ cross: true
+
+ toolchain:
+ - stable
+
steps:
- - uses: actions/checkout@v4
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Cache cargo & target directories
+ uses: Swatinem/rust-cache@v2
+ with:
+ key: "publish-connectors-${{ matrix.platform.target }}"
+
+ - name: Install musl-tools on Linux
+ run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools
+ if: contains(matrix.platform.name, 'musl')
- - run: |
+ - name: Prepare ${{ matrix.platform.target }} toolchain
+ run: |
rustup toolchain add --profile=minimal stable
+ rustup target add --toolchain=stable ${{ matrix.platform.target }}
rustup override set stable
- - name: publish
+ - name: Install cross
+ uses: taiki-e/install-action@v2
+ with:
+ tool: cross
+
+ - name: Build iggy-connectors ${{ matrix.platform.target }} release binary
+ run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} --bin iggy-connectors
+
+ - name: Build iggy-cli ${{ matrix.platform.target }} release binary
+ run: cross +stable build --verbose --release --no-default-features --target ${{ matrix.platform.target }} --bin iggy
+
+ - name: Prepare artifacts
+ run: |
+ mkdir -p release_artifacts/
+ cp target/${{ matrix.platform.target }}/release/iggy-connectors release_artifacts/
+ cp target/${{ matrix.platform.target }}/release/iggy release_artifacts/
+
+ - name: Print message
+ run: echo "::notice ::Published ${{ needs.tag.outputs.connectors_version }} release artifacts on GitHub"
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+ with:
+ platforms: "arm64,arm"
+ if: ${{ matrix.platform.cross }}
+
+ - name: Set up Docker
+ uses: crazy-max/ghaction-setup-docker@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USER }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Docker meta
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.DOCKERHUB_REGISTRY_NAME }}
+
+ - name: Build and push by digest
+ id: build
+ uses: docker/build-push-action@v6
+ with:
+ context: ./
+ file: ./Dockerfile.connectors.ci
+ platforms: ${{ matrix.platform.docker_arch }}
+ labels: ${{ steps.meta.outputs.labels }}
+ outputs: type=image,name=${{ env.DOCKERHUB_REGISTRY_NAME }},push-by-digest=true,name-canonical=true,push=true
+ build-args: |
+ IGGY_CONNECTORS_PATH=target/${{ matrix.platform.target }}/release/iggy-connectors
+
+ - name: Export digest
run: |
- cargo login "${{ secrets.CARGO_REGISTRY_TOKEN }}"
- cargo publish -p iggy-connectors
+ mkdir -p /tmp/digests
+ digest="${{ steps.build.outputs.digest }}"
+ touch "/tmp/digests/${digest#sha256:}"
+
+ - name: Upload digest
+ uses: actions/upload-artifact@v4
+ with:
+ name: digests-${{ matrix.platform.os_name }}
+ path: /tmp/digests/*
+ if-no-files-found: error
+ retention-days: 1
- finalize_connectors:
+ merge_docker_manifest:
runs-on: ubuntu-latest
needs:
- - publish
+ - release_and_publish
+ - tag
+ steps:
+ - name: Download digests
+ uses: actions/download-artifact@v4
+ with:
+ pattern: "digests-*"
+ merge-multiple: true
+ path: /tmp/digests
+
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USER }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Docker meta (tag was created)
+ if: ${{ needs.tag.outputs.tag_created == 'true' }}
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.DOCKERHUB_REGISTRY_NAME }}
+ tags: |
+ ${{ needs.tag.outputs.connectors_version }}
+ latest
+
+ - name: Docker meta (tag was not created)
+ if: ${{ needs.tag.outputs.tag_created == '' }}
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.DOCKERHUB_REGISTRY_NAME }}
+ tags: |
+ edge
+
+ - name: Create manifest list and push
+ working-directory: /tmp/digests
+ run: |
+ docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
+ $(printf '${{ env.DOCKERHUB_REGISTRY_NAME }}@sha256:%s ' *)
+
+ - name: Inspect image
+ run: |
+ docker buildx imagetools inspect ${{ env.DOCKERHUB_REGISTRY_NAME }}:latest
+
+ github_release:
+ uses: ./.github/workflows/release_connectors.yml
+ needs: tag
+ if: ${{ needs.tag.outputs.tag_created == 'true' }}
+ with:
+ tag_name: "connectors-${{ needs.tag.outputs.connectors_version }}"
+
+ finalize_publish_connectors:
+ runs-on: ubuntu-latest
+ needs:
+ - release_and_publish
+ - merge_docker_manifest
+ - github_release
if: always()
steps:
- - uses: actions/checkout@v4
+ - name: Checkout code
+ uses: actions/checkout@v4
+
- name: Everything is fine
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
+
- name: Something went wrong
- if: ${{ contains(needs.*.result, 'failure') }}
+ if: ${{ contains(needs.*.result, 'failure') && github.event_name != 'workflow_dispatch' }}
uses: JasonEtco/create-an-issue@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GITHUB_BOT_CONTEXT_STRING: "publish to crates.io"
+ GITHUB_BOT_CONTEXT_STRING: "build and release to dockerhub"
with:
filename: .github/BOT_ISSUE_TEMPLATE.md
diff --git a/.github/workflows/publish_server.yml b/.github/workflows/publish_mcp.yml
similarity index 80%
copy from .github/workflows/publish_server.yml
copy to .github/workflows/publish_mcp.yml
index c860c666..6bcad502 100644
--- a/.github/workflows/publish_server.yml
+++ b/.github/workflows/publish_mcp.yml
@@ -15,16 +15,16 @@
# specific language governing permissions and limitations
# under the License.
-name: publish_server
+name: publish_mcp
on:
push:
tags:
- - 'server-*'
+ - "mcp-*"
env:
- DOCKERHUB_REGISTRY_NAME: apache/iggy
- CRATE_NAME: iggy
+ DOCKERHUB_REGISTRY_NAME: apache/iggy-mcp
+ CRATE_NAME: iggy-mcp
GITHUB_TOKEN: ${{ github.token }}
RUST_BACKTRACE: 1
CARGO_TERM_COLOR: always
@@ -32,7 +32,7 @@ env:
jobs:
validate:
- if: startsWith(github.ref, 'refs/tags/server-')
+ if: startsWith(github.ref, 'refs/tags/mcp-')
runs-on: ubuntu-latest
steps:
- name: Extract tag name
@@ -42,8 +42,8 @@ jobs:
- name: Validate tag format
run: |
TAG=${TAG}
- if [[ ! "$TAG" =~ ^server-([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
- echo "Tag $TAG does not match strict semver format (server-X.Y.Z where 0 <= X,Y,Z <= 999)"
+ if [[ ! "$TAG" =~ ^mcp-([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
+ echo "Tag $TAG does not match strict semver format (mcp-X.Y.Z where 0 <= X,Y,Z <= 999)"
exit 1
fi
echo "Valid tag: $TAG"
@@ -64,17 +64,17 @@ jobs:
echo "tag_name=$tag" >> "$GITHUB_OUTPUT"
echo "::notice ::Tag that triggered the workflow: $tag"
- - name: Extract iggy-server version from Cargo.toml
+ - name: Extract iggy-mcp version from Cargo.toml
id: extract_version
run: |
- version=$(cargo pkgid -p server | cut -d# -f2 | cut -d: -f2)
- echo "server_version=$version" >> "$GITHUB_OUTPUT"
+ version=$(cargo pkgid -p iggy-mcp | cut -d# -f2 | cut -d: -f2)
+ echo "mcp_version=$version" >> "$GITHUB_OUTPUT"
echo "::notice ::Version from Cargo.toml $version"
- name: Check if version from Cargo.toml is the same as the tag
id: check_git_tag
run: |
- if [[ "server-${{ steps.extract_version.outputs.server_version }}" == "${{ steps.extract_tag.outputs.tag_name }}" ]];
+ if [[ "mcp-${{ steps.extract_version.outputs.mcp_version }}" == "${{ steps.extract_tag.outputs.tag_name }}" ]];
then
echo "::notice ::Tag ${{ steps.extract_tag.outputs.tag_name }} matches the version in Cargo.toml"
echo "tag_matches=true" >> "$GITHUB_OUTPUT"
@@ -84,7 +84,7 @@ jobs:
fi
outputs:
- server_version: ${{ steps.extract_version.outputs.server_version }}
+ mcp_version: ${{ steps.extract_version.outputs.mcp_version }}
tag_created: ${{ steps.check_git_tag.outputs.tag_matches }}
release_and_publish:
@@ -98,9 +98,8 @@ jobs:
os: ubuntu-latest
target: x86_64-unknown-linux-musl
bin:
- - iggy-server
- - iggy
- name: iggy-Linux-x86_64-musl.tar.gz
+ - iggy-mcp
+ name: iggy-mcp-Linux-x86_64-musl.tar.gz
cargo_command: cargo
docker_arch: linux/amd64
cross: false
@@ -109,9 +108,8 @@ jobs:
os: ubuntu-latest
target: aarch64-unknown-linux-musl
bin:
- - iggy-server
- - iggy
- name: iggy-Linux-aarch64-musl.tar.gz
+ - iggy-mcp
+ name: iggy-mcp-Linux-aarch64-musl.tar.gz
docker_arch: linux/arm64/v8
cross: true
@@ -125,7 +123,7 @@ jobs:
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
with:
- key: "publish-server-${{ matrix.platform.target }}"
+ key: "publish-mcp-${{ matrix.platform.target }}"
- name: Install musl-tools on Linux
run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools
@@ -133,17 +131,17 @@ jobs:
- name: Prepare ${{ matrix.platform.target }} toolchain
run: |
- rustup toolchain add --profile=minimal stable
- rustup target add --toolchain=stable ${{ matrix.platform.target }}
- rustup override set stable
+ rustup toolchain add --profile=minimal stable
+ rustup target add --toolchain=stable ${{ matrix.platform.target }}
+ rustup override set stable
- name: Install cross
uses: taiki-e/install-action@v2
with:
tool: cross
- - name: Build iggy-server ${{ matrix.platform.target }} release binary
- run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} --bin iggy-server
+ - name: Build iggy-mcp ${{ matrix.platform.target }} release binary
+ run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} --bin iggy-mcp
- name: Build iggy-cli ${{ matrix.platform.target }} release binary
run: cross +stable build --verbose --release --no-default-features --target ${{ matrix.platform.target }} --bin iggy
@@ -151,11 +149,11 @@ jobs:
- name: Prepare artifacts
run: |
mkdir -p release_artifacts/
- cp target/${{ matrix.platform.target }}/release/iggy-server release_artifacts/
+ cp target/${{ matrix.platform.target }}/release/iggy-mcp release_artifacts/
cp target/${{ matrix.platform.target }}/release/iggy release_artifacts/
- name: Print message
- run: echo "::notice ::Published ${{ needs.tag.outputs.server_version }} release artifacts on GitHub"
+ run: echo "::notice ::Published ${{ needs.tag.outputs.mcp }} release artifacts on GitHub"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
@@ -186,13 +184,12 @@ jobs:
uses: docker/build-push-action@v6
with:
context: ./
- file: ./Dockerfile.ci
+ file: ./Dockerfile.mcp.ci
platforms: ${{ matrix.platform.docker_arch }}
labels: ${{ steps.meta.outputs.labels }}
outputs: type=image,name=${{ env.DOCKERHUB_REGISTRY_NAME }},push-by-digest=true,name-canonical=true,push=true
build-args: |
- IGGY_CMD_PATH=target/${{ matrix.platform.target }}/release/iggy
- IGGY_SERVER_PATH=target/${{ matrix.platform.target }}/release/iggy-server
+ IGGY_MCP_PATH=target/${{ matrix.platform.target }}/release/iggy-mcp
- name: Export digest
run: |
@@ -217,7 +214,7 @@ jobs:
- name: Download digests
uses: actions/download-artifact@v4
with:
- pattern: 'digests-*'
+ pattern: "digests-*"
merge-multiple: true
path: /tmp/digests
@@ -236,7 +233,7 @@ jobs:
with:
images: ${{ env.DOCKERHUB_REGISTRY_NAME }}
tags: |
- ${{ needs.tag.outputs.server_version }}
+ ${{ needs.tag.outputs.mcp_version }}
latest
- name: Docker meta (tag was not created)
@@ -258,13 +255,13 @@ jobs:
docker buildx imagetools inspect ${{ env.DOCKERHUB_REGISTRY_NAME }}:latest
github_release:
- uses: ./.github/workflows/release_server.yml
+ uses: ./.github/workflows/release_mcp.yml
needs: tag
if: ${{ needs.tag.outputs.tag_created == 'true' }}
with:
- tag_name: "server-${{ needs.tag.outputs.server_version }}"
+ tag_name: "mcp-${{ needs.tag.outputs.mcp_version }}"
- finalize_publish_server:
+ finalize_publish_mcp:
runs-on: ubuntu-latest
needs:
- release_and_publish
diff --git a/.github/workflows/publish_server.yml b/.github/workflows/publish_server.yml
index c860c666..33c9c857 100644
--- a/.github/workflows/publish_server.yml
+++ b/.github/workflows/publish_server.yml
@@ -20,7 +20,7 @@ name: publish_server
on:
push:
tags:
- - 'server-*'
+ - "server-*"
env:
DOCKERHUB_REGISTRY_NAME: apache/iggy
@@ -133,9 +133,9 @@ jobs:
- name: Prepare ${{ matrix.platform.target }} toolchain
run: |
- rustup toolchain add --profile=minimal stable
- rustup target add --toolchain=stable ${{ matrix.platform.target }}
- rustup override set stable
+ rustup toolchain add --profile=minimal stable
+ rustup target add --toolchain=stable ${{ matrix.platform.target }}
+ rustup override set stable
- name: Install cross
uses: taiki-e/install-action@v2
@@ -217,7 +217,7 @@ jobs:
- name: Download digests
uses: actions/download-artifact@v4
with:
- pattern: 'digests-*'
+ pattern: "digests-*"
merge-multiple: true
path: /tmp/digests
diff --git a/.github/workflows/release_bench.yml b/.github/workflows/release_bench.yml
index 2548c866..6fc94122 100644
--- a/.github/workflows/release_bench.yml
+++ b/.github/workflows/release_bench.yml
@@ -39,31 +39,31 @@ jobs:
platform:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- executable: iggy
+ executable: iggy-bench
file: iggy-bench-x86_64-unknown-linux-musl.tgz
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
- executable: iggy
+ executable: iggy-bench
file: iggy-bench-aarch64-unknown-linux-musl.tgz
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- executable: iggy
+ executable: iggy-bench
file: iggy-bench-x86_64-unknown-linux-gnu.tgz
- target: x86_64-pc-windows-msvc
os: windows-latest
- executable: iggy.exe
+ executable: iggy-bench.exe
file: iggy-bench-x86_64-pc-windows-msvc.zip
- target: aarch64-pc-windows-msvc
os: windows-latest
- executable: iggy.exe
+ executable: iggy-bench.exe
file: iggy-bench-aarch64-pc-windows-msvc.zip
- target: x86_64-apple-darwin
os: macos-latest
- executable: iggy
+ executable: iggy-bench
file: iggy-bench-x86_64-apple-darwin.zip
- target: aarch64-apple-darwin
os: macos-latest
- executable: iggy
+ executable: iggy-bench
file: iggy-bench-aarch64-apple-darwin.zip
steps:
- name: Checkout code
@@ -90,7 +90,7 @@ jobs:
tool: cross
- name: Build ${{ matrix.platform.target }} release binary
- run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} -p iggy
+ run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} -p iggy-bench
- name: Collect ${{ matrix.platform.target }} executable
run: |
diff --git a/.github/workflows/release_connectors.yml b/.github/workflows/release_connectors.yml
index a87adb6b..9daf10a1 100644
--- a/.github/workflows/release_connectors.yml
+++ b/.github/workflows/release_connectors.yml
@@ -39,31 +39,31 @@ jobs:
platform:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- executable: iggy
+ executable: iggy-connectors
file: iggy-connectors-x86_64-unknown-linux-musl.tgz
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
- executable: iggy
+ executable: iggy-connectors
file: iggy-connectors-aarch64-unknown-linux-musl.tgz
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- executable: iggy
+ executable: iggy-connectors
file: iggy-connectors-x86_64-unknown-linux-gnu.tgz
- target: x86_64-pc-windows-msvc
os: windows-latest
- executable: iggy.exe
+ executable: iggy-connectors.exe
file: iggy-connectors-x86_64-pc-windows-msvc.zip
- target: aarch64-pc-windows-msvc
os: windows-latest
- executable: iggy.exe
+ executable: iggy-connectors.exe
file: iggy-connectors-aarch64-pc-windows-msvc.zip
- target: x86_64-apple-darwin
os: macos-latest
- executable: iggy
+ executable: iggy-connectors
file: iggy-connectors-x86_64-apple-darwin.zip
- target: aarch64-apple-darwin
os: macos-latest
- executable: iggy
+ executable: iggy-connectors
file: iggy-connectors-aarch64-apple-darwin.zip
steps:
- name: Checkout code
@@ -90,7 +90,7 @@ jobs:
tool: cross
- name: Build ${{ matrix.platform.target }} release binary
- run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} -p iggy
+ run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} -p iggy-connectors
- name: Collect ${{ matrix.platform.target }} executable
run: |
diff --git a/.github/workflows/release_bench.yml b/.github/workflows/release_mcp.yml
similarity index 82%
copy from .github/workflows/release_bench.yml
copy to .github/workflows/release_mcp.yml
index 2548c866..c944790d 100644
--- a/.github/workflows/release_bench.yml
+++ b/.github/workflows/release_mcp.yml
@@ -15,7 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-name: release_bench
+name: release_mcp
on:
workflow_call:
@@ -32,39 +32,39 @@ env:
IGGY_CI_BUILD: true
jobs:
- build_bench:
+ build_mcp:
runs-on: ${{ matrix.platform.os }}
strategy:
matrix:
platform:
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
- executable: iggy
- file: iggy-bench-x86_64-unknown-linux-musl.tgz
+ executable: iggy-mcp
+ file: iggy-mcp-x86_64-unknown-linux-musl.tgz
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
- executable: iggy
- file: iggy-bench-aarch64-unknown-linux-musl.tgz
+ executable: iggy-mcp
+ file: iggy-mcp-aarch64-unknown-linux-musl.tgz
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
- executable: iggy
- file: iggy-bench-x86_64-unknown-linux-gnu.tgz
+ executable: iggy-mcp
+ file: iggy-mcp-x86_64-unknown-linux-gnu.tgz
- target: x86_64-pc-windows-msvc
os: windows-latest
- executable: iggy.exe
- file: iggy-bench-x86_64-pc-windows-msvc.zip
+ executable: iggy-mcp.exe
+ file: iggy-mcp-x86_64-pc-windows-msvc.zip
- target: aarch64-pc-windows-msvc
os: windows-latest
- executable: iggy.exe
- file: iggy-bench-aarch64-pc-windows-msvc.zip
+ executable: iggy-mcp.exe
+ file: iggy-mcp-aarch64-pc-windows-msvc.zip
- target: x86_64-apple-darwin
os: macos-latest
- executable: iggy
- file: iggy-bench-x86_64-apple-darwin.zip
+ executable: iggy-mcp
+ file: iggy-mcp-x86_64-apple-darwin.zip
- target: aarch64-apple-darwin
os: macos-latest
- executable: iggy
- file: iggy-bench-aarch64-apple-darwin.zip
+ executable: iggy-mcp
+ file: iggy-mcp-aarch64-apple-darwin.zip
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -72,7 +72,7 @@ jobs:
- name: Cache cargo & target directories
uses: Swatinem/rust-cache@v2
with:
- key: "release-bench-${{ matrix.platform.target }}"
+ key: "release-mcp-${{ matrix.platform.target }}"
- name: Install musl-tools on Linux
run: sudo apt-get update --yes && sudo apt-get install --yes musl-tools
@@ -90,7 +90,7 @@ jobs:
tool: cross
- name: Build ${{ matrix.platform.target }} release binary
- run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} -p iggy
+ run: cross +stable build --verbose --release --target ${{ matrix.platform.target }} -p iggy-mcp
- name: Collect ${{ matrix.platform.target }} executable
run: |
diff --git a/Cargo.lock b/Cargo.lock
index ba8123a2..4db7dd24 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -77,7 +77,7 @@ dependencies = [
"flate2",
"foldhash",
"futures-core",
- "h2 0.3.26",
+ "h2 0.3.27",
"http 0.2.12",
"httparse",
"httpdate",
@@ -87,7 +87,7 @@ dependencies = [
"mime",
"percent-encoding",
"pin-project-lite",
- "rand 0.9.1",
+ "rand 0.9.2",
"sha1",
"smallvec",
"tokio",
@@ -143,7 +143,7 @@ dependencies = [
"futures-core",
"futures-util",
"mio",
- "socket2",
+ "socket2 0.5.10",
"tokio",
"tracing",
]
@@ -205,7 +205,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"smallvec",
- "socket2",
+ "socket2 0.5.10",
"time",
"tracing",
"url",
@@ -468,11 +468,11 @@ dependencies = [
[[package]]
name = "async-compression"
-version = "0.4.25"
+version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40f6024f3f856663b45fd0c9b6f2024034a702f453549449e0d84a305900dad4"
+checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8"
dependencies = [
- "bzip2 0.6.0",
+ "bzip2",
"flate2",
"futures-core",
"futures-io",
@@ -665,9 +665,9 @@ dependencies = [
[[package]]
name = "aws-lc-rs"
-version = "1.13.1"
+version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93fcc8f365936c834db5514fc45aee5b1202d677e6b40e48468aaaa8183ca8c7"
+checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -675,9 +675,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.29.0"
+version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61b1d86e7705efe1be1b569bab41d4fa1e14e220b60a160f78de2db687add079"
+checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff"
dependencies = [
"bindgen",
"cc",
@@ -948,10 +948,10 @@ dependencies = [
"derive-new",
"derive_more 2.0.1",
"human-repr",
- "rand 0.9.1",
+ "rand 0.9.2",
"serde",
"serde_json",
- "sysinfo 0.35.2",
+ "sysinfo 0.36.1",
"tracing",
"uuid",
]
@@ -1083,9 +1083,9 @@ dependencies = [
[[package]]
name = "bon"
-version = "3.6.4"
+version = "3.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f61138465baf186c63e8d9b6b613b508cd832cba4ce93cf37ce5f096f91ac1a6"
+checksum = "33d9ef19ae5263a138da9a86871eca537478ab0332a7770bac7e3f08b801f89f"
dependencies = [
"bon-macros",
"rustversion",
@@ -1093,11 +1093,11 @@ dependencies = [
[[package]]
name = "bon-macros"
-version = "3.6.4"
+version = "3.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40d1dad34aa19bf02295382f08d9bc40651585bd497266831d40ee6296fb49ca"
+checksum = "577ae008f2ca11ca7641bd44601002ee5ab49ef0af64846ce1ab6057218a5cc1"
dependencies = [
- "darling",
+ "darling 0.21.0",
"ident_case",
"prettyplease",
"proc-macro2",
@@ -1169,9 +1169,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.18.1"
+version = "3.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
[[package]]
name = "byte-unit"
@@ -1239,15 +1239,6 @@ dependencies = [
"bytes",
]
-[[package]]
-name = "bzip2"
-version = "0.5.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
-dependencies = [
- "bzip2-sys",
-]
-
[[package]]
name = "bzip2"
version = "0.6.0"
@@ -1257,16 +1248,6 @@ dependencies = [
"libbz2-rs-sys",
]
-[[package]]
-name = "bzip2-sys"
-version = "0.1.13+1.0.8"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
-dependencies = [
- "cc",
- "pkg-config",
-]
-
[[package]]
name = "camino"
version = "1.1.10"
@@ -1301,18 +1282,18 @@ dependencies = [
[[package]]
name = "castaway"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
dependencies = [
"rustversion",
]
[[package]]
name = "cc"
-version = "1.2.27"
+version = "1.2.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc"
+checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7"
dependencies = [
"jobserver",
"libc",
@@ -1412,9 +1393,9 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.40"
+version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
+checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
dependencies = [
"clap_builder",
"clap_derive",
@@ -1422,9 +1403,9 @@ dependencies = [
[[package]]
name = "clap_builder"
-version = "4.5.40"
+version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
+checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
dependencies = [
"anstream",
"anstyle",
@@ -1435,18 +1416,18 @@ dependencies = [
[[package]]
name = "clap_complete"
-version = "4.5.54"
+version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aad5b1b4de04fead402672b48897030eec1f3bfe1550776322f59f6d6e6a5677"
+checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
-version = "4.5.40"
+version = "4.5.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
+checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@@ -1471,9 +1452,12 @@ dependencies = [
[[package]]
name = "cobs"
-version = "0.2.3"
+version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror 2.0.12",
+]
[[package]]
name = "colorchoice"
@@ -1535,9 +1519,9 @@ dependencies = [
[[package]]
name = "config"
-version = "0.15.11"
+version = "0.15.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595aae20e65c3be792d05818e8c63025294ac3cb7e200f11459063a352a6ef80"
+checksum = "5b1eb4fb07bc7f012422df02766c7bd5971effb894f573865642f06fa3265440"
dependencies = [
"async-trait",
"convert_case 0.6.0",
@@ -1547,8 +1531,8 @@ dependencies = [
"rust-ini",
"serde",
"serde_json",
- "toml",
- "winnow 0.7.11",
+ "toml 0.9.2",
+ "winnow 0.7.12",
"yaml-rust2",
]
@@ -1572,8 +1556,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8030735ecb0d128428b64cd379809817e620a40e5001c54465b99ec5feec2857"
dependencies = [
"futures-core",
- "prost",
- "prost-types",
+ "prost 0.13.5",
+ "prost-types 0.13.5",
"tonic 0.12.3",
"tracing-core",
]
@@ -1591,8 +1575,8 @@ dependencies = [
"hdrhistogram",
"humantime",
"hyper-util",
- "prost",
- "prost-types",
+ "prost 0.13.5",
+ "prost-types 0.13.5",
"serde",
"serde_json",
"thread_local",
@@ -1723,9 +1707,9 @@ checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
-version = "1.4.2"
+version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
dependencies = [
"cfg-if",
]
@@ -1816,9 +1800,9 @@ dependencies = [
[[package]]
name = "crunchy"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43da5946c66ffcc7745f48db692ffbb10a83bfe0afd96235c5c2a4fb23994929"
+checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
[[package]]
name = "crypto-common"
@@ -1921,8 +1905,18 @@ version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
- "darling_core",
- "darling_macro",
+ "darling_core 0.20.11",
+ "darling_macro 0.20.11",
+]
+
+[[package]]
+name = "darling"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a79c4acb1fd5fa3d9304be4c76e031c54d2e92d172a393e24b19a14fe8532fe9"
+dependencies = [
+ "darling_core 0.21.0",
+ "darling_macro 0.21.0",
]
[[package]]
@@ -1939,13 +1933,38 @@ dependencies = [
"syn 2.0.104",
]
+[[package]]
+name = "darling_core"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "74875de90daf30eb59609910b84d4d368103aaec4c924824c6799b28f77d6a1d"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.104",
+]
+
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
- "darling_core",
+ "darling_core 0.20.11",
+ "quote",
+ "syn 2.0.104",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e79f8e61677d5df9167cd85265f8e5f64b215cdea3fb55eebc3e622e44c7a146"
+dependencies = [
+ "darling_core 0.21.0",
"quote",
"syn 2.0.104",
]
@@ -2053,7 +2072,7 @@ version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
- "darling",
+ "darling 0.20.11",
"proc-macro2",
"quote",
"syn 2.0.104",
@@ -2448,7 +2467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27cea6e7f512d43b098939ff4d5a5d6fe3db07971e1d05176fe26c642d33f5b8"
dependencies = [
"getrandom 0.3.3",
- "rand 0.9.1",
+ "rand 0.9.2",
"siphasher",
"wide",
]
@@ -2475,39 +2494,27 @@ dependencies = [
"pear",
"serde",
"serde_json",
- "toml",
+ "toml 0.8.23",
"uncased",
"version_check",
]
[[package]]
name = "file-operation"
-version = "0.5.12"
+version = "0.5.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b1b81e9a35a431f57f3304aec4ed6a59c7f1fb82af62b02e18afe197e8e3769d"
+checksum = "db833ed44bb54a24e43acc553147e7d0a8f1111128b261646afd11c28151a79c"
dependencies = [
"tokio",
]
-[[package]]
-name = "filetime"
-version = "0.2.25"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
-dependencies = [
- "cfg-if",
- "libc",
- "libredox",
- "windows-sys 0.59.0",
-]
-
[[package]]
name = "flatbuffers"
-version = "24.12.23"
+version = "25.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f1baf0dbf96932ec9a3038d57900329c015b0bfb7b63d904f3bc27e2b02a096"
+checksum = "1045398c1bfd89168b5fd3f1fc11f6e70b34f6f66300c87d44d3de849463abf1"
dependencies = [
- "bitflags 1.3.2",
+ "bitflags 2.9.1",
"rustc_version",
]
@@ -3288,7 +3295,7 @@ dependencies = [
"parking_lot 0.12.4",
"portable-atomic",
"quanta",
- "rand 0.9.1",
+ "rand 0.9.2",
"smallvec",
"spinning_top",
"web-time",
@@ -3296,9 +3303,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.3.26"
+version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8"
+checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d"
dependencies = [
"bytes",
"fnv",
@@ -3306,7 +3313,7 @@ dependencies = [
"futures-sink",
"futures-util",
"http 0.2.12",
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
"slab",
"tokio",
"tokio-util",
@@ -3315,9 +3322,9 @@ dependencies = [
[[package]]
name = "h2"
-version = "0.4.10"
+version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5"
+checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
dependencies = [
"atomic-waker",
"bytes",
@@ -3325,7 +3332,7 @@ dependencies = [
"futures-core",
"futures-sink",
"http 1.3.1",
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
"slab",
"tokio",
"tokio-util",
@@ -3575,7 +3582,7 @@ dependencies = [
"bytes",
"futures-channel",
"futures-util",
- "h2 0.4.10",
+ "h2 0.4.11",
"http 1.3.1",
"http-body",
"httparse",
@@ -3603,7 +3610,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tower-service",
- "webpki-roots 1.0.1",
+ "webpki-roots 1.0.2",
]
[[package]]
@@ -3621,9 +3628,9 @@ dependencies = [
[[package]]
name = "hyper-util"
-version = "0.1.14"
+version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb"
+checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -3637,7 +3644,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
- "socket2",
+ "socket2 0.6.0",
"tokio",
"tower-service",
"tracing",
@@ -3807,7 +3814,7 @@ dependencies = [
"tokio-rustls",
"tracing",
"trait-variant",
- "webpki-roots 1.0.1",
+ "webpki-roots 1.0.2",
]
[[package]]
@@ -3828,12 +3835,12 @@ dependencies = [
"iggy",
"integration",
"nonzero_lit",
- "rand 0.9.1",
+ "rand 0.9.2",
"rayon",
"serde",
- "sysinfo 0.35.2",
+ "sysinfo 0.36.1",
"tokio",
- "toml",
+ "toml 0.9.2",
"tracing",
"tracing-appender",
"tracing-subscriber",
@@ -3884,7 +3891,27 @@ dependencies = [
"strum",
"thiserror 2.0.12",
"tokio",
- "toml",
+ "toml 0.9.2",
+ "tower-http",
+ "tracing",
+ "tracing-subscriber",
+]
+
+[[package]]
+name = "iggy-mcp"
+version = "0.1.0"
+dependencies = [
+ "axum 0.8.4",
+ "axum-server",
+ "config",
+ "dotenvy",
+ "figlet-rs",
+ "iggy",
+ "rmcp",
+ "serde",
+ "strum",
+ "thiserror 2.0.12",
+ "tokio",
"tower-http",
"tracing",
"tracing-subscriber",
@@ -3907,7 +3934,7 @@ dependencies = [
"serde",
"serde_json",
"tokio",
- "toml",
+ "toml 0.9.2",
"tracing",
]
@@ -4000,7 +4027,7 @@ dependencies = [
"humantime",
"iggy_connector_sdk",
"once_cell",
- "rand 0.9.1",
+ "rand 0.9.2",
"serde",
"simd-json",
"tokio",
@@ -4020,8 +4047,8 @@ dependencies = [
"iggy",
"once_cell",
"postcard",
- "prost",
- "prost-types",
+ "prost 0.14.1",
+ "prost-types 0.14.1",
"protox",
"protox-parse",
"regex",
@@ -4060,7 +4087,7 @@ dependencies = [
"clap",
"futures-util",
"iggy",
- "rand 0.9.1",
+ "rand 0.9.2",
"serde",
"serde_json",
"thiserror 2.0.12",
@@ -4098,7 +4125,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a9aa791c7b5a71b636b7a68207fdebf171ddfc593d9c8506ec4cbc527b6a84"
dependencies = [
"implicit-clone-derive",
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
]
[[package]]
@@ -4124,9 +4151,9 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.9.0"
+version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
+checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
dependencies = [
"equivalent",
"hashbrown 0.15.4",
@@ -4214,7 +4241,10 @@ dependencies = [
"libc",
"log",
"predicates",
+ "rand 0.9.2",
"rcgen",
+ "reqwest",
+ "rmcp",
"serial_test",
"server",
"tempfile",
@@ -4234,6 +4264,17 @@ dependencies = [
"rustversion",
]
+[[package]]
+name = "io-uring"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "libc",
+]
+
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -4573,13 +4614,12 @@ dependencies = [
[[package]]
name = "libredox"
-version = "0.1.3"
+version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+checksum = "4488594b9328dee448adb906d8b126d9b7deb7cf5c22161ee591610bb1be83c0"
dependencies = [
"bitflags 2.9.1",
"libc",
- "redox_syscall 0.5.13",
]
[[package]]
@@ -5002,12 +5042,11 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
[[package]]
name = "notify"
-version = "8.0.0"
+version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
+checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
dependencies = [
"bitflags 2.9.1",
- "filetime",
"fsevent-sys",
"inotify",
"kqueue",
@@ -5016,7 +5055,7 @@ dependencies = [
"mio",
"notify-types",
"walkdir",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
@@ -5316,9 +5355,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-src"
-version = "300.5.0+3.5.0"
+version = "300.5.1+3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8ce546f549326b0e6052b649198487d91320875da901e7bd11a06d1ee3f9c2f"
+checksum = "735230c832b28c000e3bc117119e6466a663ec73506bc0a9907ea4187508e42a"
dependencies = [
"cc",
]
@@ -5387,7 +5426,7 @@ dependencies = [
"opentelemetry-http",
"opentelemetry-proto",
"opentelemetry_sdk",
- "prost",
+ "prost 0.13.5",
"reqwest",
"thiserror 2.0.12",
"tokio",
@@ -5403,7 +5442,7 @@ checksum = "2e046fd7660710fe5a05e8748e70d9058dc15c94ba914e7c4faa7c728f0e8ddc"
dependencies = [
"opentelemetry",
"opentelemetry_sdk",
- "prost",
+ "prost 0.13.5",
"tonic 0.13.1",
]
@@ -5424,7 +5463,7 @@ dependencies = [
"futures-util",
"opentelemetry",
"percent-encoding",
- "rand 0.9.1",
+ "rand 0.9.2",
"serde_json",
"thiserror 2.0.12",
"tokio",
@@ -5502,7 +5541,7 @@ checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if",
"libc",
- "redox_syscall 0.5.13",
+ "redox_syscall 0.5.15",
"smallvec",
"windows-targets 0.52.6",
]
@@ -5763,9 +5802,9 @@ dependencies = [
[[package]]
name = "postcard"
-version = "1.1.1"
+version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "170a2601f67cc9dba8edd8c4870b15f71a6a2dc196daec8c83f72b59dff628a8"
+checksum = "6c1de96e20f51df24ca73cafcc4690e044854d803259db27a00a461cb3b9d17a"
dependencies = [
"cobs",
"embedded-io 0.4.0",
@@ -5789,6 +5828,12 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+[[package]]
+name = "ppmd-rust"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c834641d8ad1b348c9ee86dec3b9840d805acd5f24daa5f90c788951a52ff59b"
+
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -5950,7 +5995,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5"
dependencies = [
"bytes",
- "prost-derive",
+ "prost-derive 0.13.5",
+]
+
+[[package]]
+name = "prost"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d"
+dependencies = [
+ "bytes",
+ "prost-derive 0.14.1",
]
[[package]]
@@ -5966,16 +6021,29 @@ dependencies = [
"syn 2.0.104",
]
+[[package]]
+name = "prost-derive"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
+dependencies = [
+ "anyhow",
+ "itertools 0.14.0",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
[[package]]
name = "prost-reflect"
-version = "0.15.3"
+version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37587d5a8a1b3dc9863403d084fc2254b91ab75a702207098837950767e2260b"
+checksum = "6e6b545c556471033fc1099868fca468bff8cf034f7bb9153b445f24d00aa28d"
dependencies = [
"logos",
"miette",
- "prost",
- "prost-types",
+ "prost 0.14.1",
+ "prost-types 0.14.1",
]
[[package]]
@@ -5984,33 +6052,42 @@ version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16"
dependencies = [
- "prost",
+ "prost 0.13.5",
+]
+
+[[package]]
+name = "prost-types"
+version = "0.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72"
+dependencies = [
+ "prost 0.14.1",
]
[[package]]
name = "protox"
-version = "0.8.0"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "424c2bd294b69c49b949f3619362bc3c5d28298cd1163b6d1a62df37c16461aa"
+checksum = "8555716f64c546306ddf3383065dc40d4232609e79e0a4c50e94e87d54f30fb4"
dependencies = [
"bytes",
"miette",
- "prost",
+ "prost 0.14.1",
"prost-reflect",
- "prost-types",
+ "prost-types 0.14.1",
"protox-parse",
"thiserror 2.0.12",
]
[[package]]
name = "protox-parse"
-version = "0.8.0"
+version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57927f9dbeeffcce7192404deee6157a640cbb3fe8ac11eabbe571565949ab75"
+checksum = "072eee358134396a4643dff81cfff1c255c9fbd3fb296be14bdb6a26f9156366"
dependencies = [
"logos",
"miette",
- "prost-types",
+ "prost-types 0.14.1",
"thiserror 2.0.12",
]
@@ -6082,7 +6159,7 @@ dependencies = [
"quinn-udp",
"rustc-hash 2.1.1",
"rustls",
- "socket2",
+ "socket2 0.5.10",
"thiserror 2.0.12",
"tokio",
"tracing",
@@ -6099,7 +6176,7 @@ dependencies = [
"fastbloom",
"getrandom 0.3.3",
"lru-slab",
- "rand 0.9.1",
+ "rand 0.9.2",
"ring",
"rustc-hash 2.1.1",
"rustls",
@@ -6121,7 +6198,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
- "socket2",
+ "socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
]
@@ -6160,9 +6237,9 @@ dependencies = [
[[package]]
name = "rand"
-version = "0.9.1"
+version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
+checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
@@ -6237,9 +6314,9 @@ dependencies = [
[[package]]
name = "rcgen"
-version = "0.13.2"
+version = "0.14.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
+checksum = "0068c5b3cab1d4e271e0bb6539c87563c43411cad90b057b15c79958fbeb41f7"
dependencies = [
"pem",
"ring",
@@ -6259,9 +6336,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.5.13"
+version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
+checksum = "7e8af0dde094006011e6a740d4879319439489813bd0bcdc7d821beaeeff48ec"
dependencies = [
"bitflags 2.9.1",
]
@@ -6364,9 +6441,9 @@ dependencies = [
[[package]]
name = "reqwest"
-version = "0.12.20"
+version = "0.12.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813"
+checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
dependencies = [
"base64 0.22.1",
"bytes",
@@ -6401,7 +6478,7 @@ dependencies = [
"wasm-bindgen-futures",
"wasm-streams",
"web-sys",
- "webpki-roots 1.0.1",
+ "webpki-roots 1.0.2",
]
[[package]]
@@ -6493,6 +6570,51 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "rmcp"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b008b927a85d514699ff304be84c5084598596e6cad4a6f5bc67207715fafe5f"
+dependencies = [
+ "axum 0.8.4",
+ "base64 0.22.1",
+ "bytes",
+ "chrono",
+ "futures",
+ "http 1.3.1",
+ "http-body",
+ "http-body-util",
+ "paste",
+ "pin-project-lite",
+ "rand 0.9.2",
+ "reqwest",
+ "rmcp-macros",
+ "schemars 1.0.4",
+ "serde",
+ "serde_json",
+ "sse-stream",
+ "thiserror 2.0.12",
+ "tokio",
+ "tokio-stream",
+ "tokio-util",
+ "tower-service",
+ "tracing",
+ "uuid",
+]
+
+[[package]]
+name = "rmcp-macros"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7465280d5f73f2c5c99017a04af407b2262455a149f255ad22f2b0b29087695c"
+dependencies = [
+ "darling 0.21.0",
+ "proc-macro2",
+ "quote",
+ "serde_json",
+ "syn 2.0.104",
+]
+
[[package]]
name = "ron"
version = "0.8.1"
@@ -6533,13 +6655,12 @@ dependencies = [
[[package]]
name = "rust-ini"
-version = "0.21.1"
+version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e310ef0e1b6eeb79169a1171daf9abcb87a2e17c03bee2c4bb100b55c75409f"
+checksum = "e7295b7ce3bf4806b419dc3420745998b447178b7005e2011947b38fc5aa6791"
dependencies = [
"cfg-if",
"ordered-multimap",
- "trim-in-place",
]
[[package]]
@@ -6634,22 +6755,22 @@ dependencies = [
[[package]]
name = "rustix"
-version = "1.0.7"
+version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266"
+checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
dependencies = [
"bitflags 2.9.1",
"errno",
"libc",
"linux-raw-sys 0.9.4",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
]
[[package]]
name = "rustls"
-version = "0.23.28"
+version = "0.23.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643"
+checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
dependencies = [
"aws-lc-rs",
"log",
@@ -6721,9 +6842,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
-version = "0.103.3"
+version = "0.103.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435"
+checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
dependencies = [
"aws-lc-rs",
"ring",
@@ -6810,6 +6931,32 @@ dependencies = [
"serde_json",
]
+[[package]]
+name = "schemars"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0"
+dependencies = [
+ "chrono",
+ "dyn-clone",
+ "ref-cast",
+ "schemars_derive",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "schemars_derive"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33d020396d1d138dc19f1165df7545479dcd58d93810dc5d646a16e55abefa80"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "serde_derive_internals",
+ "syn 2.0.104",
+]
+
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -6824,9 +6971,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdd"
-version = "3.0.8"
+version = "3.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21"
+checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
[[package]]
name = "seahash"
@@ -6929,11 +7076,22 @@ dependencies = [
"syn 2.0.104",
]
+[[package]]
+name = "serde_derive_internals"
+version = "0.29.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.104",
+]
+
[[package]]
name = "serde_json"
-version = "1.0.140"
+version = "1.0.141"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
+checksum = "30b9eff21ebe718216c6ec64e1d9ac57087aad11efc64e32002bce4a0d4c03d3"
dependencies = [
"itoa",
"memchr",
@@ -6960,6 +7118,15 @@ dependencies = [
"serde",
]
+[[package]]
+name = "serde_spanned"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -6974,16 +7141,17 @@ dependencies = [
[[package]]
name = "serde_with"
-version = "3.13.0"
+version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf65a400f8f66fb7b0552869ad70157166676db75ed8181f8104ea91cf9d0b42"
+checksum = "f2c45cd61fefa9db6f254525d46e392b852e0e61d9a1fd36e5bd183450a556d5"
dependencies = [
"base64 0.22.1",
"chrono",
"hex",
"indexmap 1.9.3",
- "indexmap 2.9.0",
- "schemars",
+ "indexmap 2.10.0",
+ "schemars 0.9.0",
+ "schemars 1.0.4",
"serde",
"serde_derive",
"serde_json",
@@ -6993,11 +7161,11 @@ dependencies = [
[[package]]
name = "serde_with_macros"
-version = "3.13.0"
+version = "3.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "81679d9ed988d5e9a5e6531dc3f2c28efbd639cbd1dfb628df08edea6004da77"
+checksum = "de90945e6565ce0d9a25098082ed4ee4002e047cb59892c318d66821e14bb30f"
dependencies = [
- "darling",
+ "darling 0.20.11",
"proc-macro2",
"quote",
"syn 2.0.104",
@@ -7009,7 +7177,7 @@ version = "0.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e2dd588bf1597a252c3b920e0143eb99b0f76e4e082f4c92ce34fbc9e71ddd"
dependencies = [
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
"itoa",
"libyml",
"memchr",
@@ -7095,13 +7263,13 @@ dependencies = [
"serial_test",
"static-toml",
"strum",
- "sysinfo 0.35.2",
+ "sysinfo 0.36.1",
"tempfile",
"thiserror 2.0.12",
"tokio",
"tokio-rustls",
"tokio-util",
- "toml",
+ "toml 0.9.2",
"tower-http",
"tracing",
"tracing-appender",
@@ -7277,6 +7445,16 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "socket2"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
[[package]]
name = "spin"
version = "0.9.8"
@@ -7337,7 +7515,7 @@ dependencies = [
"futures-util",
"hashbrown 0.15.4",
"hashlink",
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
"log",
"memchr",
"once_cell",
@@ -7503,6 +7681,19 @@ dependencies = [
"uuid",
]
+[[package]]
+name = "sse-stream"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb4dc4d33c68ec1f27d386b5610a351922656e1fdf5c05bbaad930cd1519479a"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http-body",
+ "http-body-util",
+ "pin-project-lite",
+]
+
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
@@ -7520,7 +7711,7 @@ dependencies = [
"proc-macro2",
"quote",
"syn 2.0.104",
- "toml",
+ "toml 0.8.23",
]
[[package]]
@@ -7548,23 +7739,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
-version = "0.27.1"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
+checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
-version = "0.27.1"
+version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
+checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
- "rustversion",
"syn 2.0.104",
]
@@ -7664,9 +7854,9 @@ dependencies = [
[[package]]
name = "sysinfo"
-version = "0.35.2"
+version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c3ffa3e4ff2b324a57f7aeb3c349656c7b127c3c189520251a648102a92496e"
+checksum = "252800745060e7b9ffb7b2badbd8b31cfa4aa2e61af879d0a3bf2a317c20217d"
dependencies = [
"libc",
"memchr",
@@ -7697,7 +7887,7 @@ dependencies = [
"fastrand",
"getrandom 0.3.3",
"once_cell",
- "rustix 1.0.7",
+ "rustix 1.0.8",
"windows-sys 0.59.0",
]
@@ -7707,7 +7897,7 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c6481c4829e4cc63825e62c49186a34538b7b2750b73b266581ffb612fb5ed"
dependencies = [
- "rustix 1.0.7",
+ "rustix 1.0.8",
"windows-sys 0.59.0",
]
@@ -7879,18 +8069,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.45.1"
+version = "1.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779"
+checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
dependencies = [
"backtrace",
"bytes",
+ "io-uring",
"libc",
"mio",
"parking_lot 0.12.4",
"pin-project-lite",
"signal-hook-registry",
- "socket2",
+ "slab",
+ "socket2 0.5.10",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
@@ -7949,11 +8141,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
dependencies = [
"serde",
- "serde_spanned",
- "toml_datetime",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
"toml_edit 0.22.27",
]
+[[package]]
+name = "toml"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
+dependencies = [
+ "indexmap 2.10.0",
+ "serde",
+ "serde_spanned 1.0.0",
+ "toml_datetime 0.7.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.12",
+]
+
[[package]]
name = "toml_datetime"
version = "0.6.11"
@@ -7963,14 +8170,23 @@ dependencies = [
"serde",
]
+[[package]]
+name = "toml_datetime"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
- "indexmap 2.9.0",
- "toml_datetime",
+ "indexmap 2.10.0",
+ "toml_datetime 0.6.11",
"winnow 0.5.40",
]
@@ -7980,12 +8196,21 @@ version = "0.22.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
dependencies = [
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
"serde",
- "serde_spanned",
- "toml_datetime",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
"toml_write",
- "winnow 0.7.11",
+ "winnow 0.7.12",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
+dependencies = [
+ "winnow 0.7.12",
]
[[package]]
@@ -7994,6 +8219,12 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+[[package]]
+name = "toml_writer"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
+
[[package]]
name = "tonic"
version = "0.12.3"
@@ -8005,7 +8236,7 @@ dependencies = [
"axum 0.7.9",
"base64 0.22.1",
"bytes",
- "h2 0.4.10",
+ "h2 0.4.11",
"http 1.3.1",
"http-body",
"http-body-util",
@@ -8014,8 +8245,8 @@ dependencies = [
"hyper-util",
"percent-encoding",
"pin-project",
- "prost",
- "socket2",
+ "prost 0.13.5",
+ "socket2 0.5.10",
"tokio",
"tokio-stream",
"tower 0.4.13",
@@ -8041,7 +8272,7 @@ dependencies = [
"hyper-util",
"percent-encoding",
"pin-project",
- "prost",
+ "prost 0.13.5",
"tokio",
"tokio-stream",
"tower 0.5.2",
@@ -8057,7 +8288,7 @@ dependencies = [
"anyhow",
"clap",
"iggy",
- "rand 0.9.1",
+ "rand 0.9.2",
"tokio",
"tracing",
"tracing-subscriber",
@@ -8091,7 +8322,7 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
"pin-project-lite",
"slab",
"sync_wrapper",
@@ -8236,12 +8467,6 @@ dependencies = [
"syn 2.0.104",
]
-[[package]]
-name = "trim-in-place"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "343e926fc669bc8cde4fa3129ab681c63671bae288b1f1081ceee6d9d37904fc"
-
[[package]]
name = "try-lock"
version = "0.2.5"
@@ -8254,7 +8479,7 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b907da542cbced5261bd3256de1b3a1bf340a3d37f93425a07362a1d687de56"
dependencies = [
- "rand 0.9.1",
+ "rand 0.9.2",
]
[[package]]
@@ -8295,7 +8520,7 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "470dbf6591da1b39d43c14523b2b469c86879a53e8b758c8e090a470fe7b1fbe"
dependencies = [
- "rand 0.9.1",
+ "rand 0.9.2",
"web-time",
]
@@ -8437,7 +8662,7 @@ checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"js-sys",
- "rand 0.9.1",
+ "rand 0.9.2",
"serde",
"wasm-bindgen",
"zerocopy",
@@ -8703,14 +8928,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e"
dependencies = [
- "webpki-root-certs 1.0.1",
+ "webpki-root-certs 1.0.2",
]
[[package]]
name = "webpki-root-certs"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "86138b15b2b7d561bc4469e77027b8dd005a43dc502e9031d1f5afc8ce1f280e"
+checksum = "4e4ffd8df1c57e87c325000a3d6ef93db75279dc3a231125aac571650f22b12a"
dependencies = [
"rustls-pki-types",
]
@@ -8721,14 +8946,14 @@ version = "0.26.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
dependencies = [
- "webpki-roots 1.0.1",
+ "webpki-roots 1.0.2",
]
[[package]]
name = "webpki-roots"
-version = "1.0.1"
+version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502"
+checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2"
dependencies = [
"rustls-pki-types",
]
@@ -8751,7 +8976,7 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
dependencies = [
- "redox_syscall 0.5.13",
+ "redox_syscall 0.5.15",
"wasite",
]
@@ -9258,9 +9483,9 @@ dependencies = [
[[package]]
name = "winnow"
-version = "0.7.11"
+version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd"
+checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
dependencies = [
"memchr",
]
@@ -9325,7 +9550,7 @@ dependencies = [
"futures",
"gloo 0.10.0",
"implicit-clone",
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
"js-sys",
"prokio",
"rustversion",
@@ -9521,23 +9746,24 @@ dependencies = [
[[package]]
name = "zip"
-version = "4.2.0"
+version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "95ab361742de920c5535880f89bbd611ee62002bf11341d16a5f057bb8ba6899"
+checksum = "9aed4ac33e8eb078c89e6cbb1d5c4c7703ec6d299fc3e7c3695af8f8b423468b"
dependencies = [
"aes",
"arbitrary",
- "bzip2 0.5.2",
+ "bzip2",
"constant_time_eq",
"crc32fast",
"deflate64",
"flate2",
"getrandom 0.3.3",
"hmac",
- "indexmap 2.9.0",
+ "indexmap 2.10.0",
"liblzma",
"memchr",
"pbkdf2",
+ "ppmd-rust",
"sha1",
"time",
"zeroize",
diff --git a/Cargo.toml b/Cargo.toml
index 241e4d92..e9ce3bd8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ codegen-units = 1
[workspace]
members = [
"bdd/rust",
+ "core/ai/mcp",
"core/bench",
"core/bench/dashboard/frontend",
"core/bench/dashboard/server",
@@ -66,7 +67,7 @@ axum-server = { version = "0.7.2", features = ["tls-rustls"] }
bcrypt = "0.17.0"
bincode = { version = "2.0.1", features = ["serde"] }
blake3 = "1.8.2"
-bon = "3.6.4"
+bon = "3.6.5"
byte-unit = { version = "5.1.6", default-features = false, features = [
"serde",
"byte",
@@ -75,10 +76,10 @@ byte-unit = { version = "5.1.6", default-features = false, features = [
bytes = "1.10.1"
charming = "0.6.0"
chrono = { version = "0.4.41", features = ["serde"] }
-clap = { version = "4.5.40", features = ["derive"] }
-config = { version = "0.15.11" }
+clap = { version = "4.5.41", features = ["derive"] }
+config = { version = "0.15.13" }
comfy-table = "7.1.4"
-crc32fast = "1.4.2"
+crc32fast = "1.5.0"
crossbeam = "0.8.4"
dashmap = "6.1.0"
derive_builder = "0.20.2"
@@ -99,27 +100,27 @@ nonzero_lit = "0.1.2"
once_cell = "1.21.3"
passterm = "=2.0.1"
quinn = "0.11.8"
-postcard = { version = "1.1.1", features = ["alloc"] }
-rand = "0.9.1"
-reqwest = { version = "0.12.20", default-features = false, features = [
+postcard = { version = "1.1.2", features = ["alloc"] }
+rand = "0.9.2"
+reqwest = { version = "0.12.22", default-features = false, features = [
"json",
"rustls-tls",
] }
reqwest-middleware = { version = "0.4.2", features = ["json"] }
reqwest-retry = "0.7.0"
-rustls = { version = "0.23.28", features = ["ring"] }
+rustls = { version = "0.23.29", features = ["ring"] }
serde = { version = "1.0.219", features = ["derive", "rc"] }
-serde_json = "1.0.140"
-serde_with = { version = "3.13.0", features = ["base64", "macros"] }
+serde_json = "1.0.141"
+serde_with = { version = "3.14.0", features = ["base64", "macros"] }
serde_yml = "0.0.12"
serial_test = "3.2.0"
simd-json = { version = "0.15.1", features = ["serde_impl"] }
-sysinfo = "0.35.2"
+sysinfo = "0.36.1"
tempfile = "3.20.0"
thiserror = "2.0.12"
-tokio = { version = "1.45.1", features = ["full"] }
+tokio = { version = "1.46.1", features = ["full"] }
tokio-rustls = "0.26.2"
-toml = "0.8.23"
+toml = "0.9.2"
tracing = "0.1.41"
tracing-appender = "0.2.3"
tracing-subscriber = { version = "0.3.19", default-features = false, features = [
@@ -138,8 +139,8 @@ rust-s3 = { version = "0.36.0-beta.2", default-features = false, features = [
"tokio-rustls-tls",
"tags",
] }
-strum = { version = "0.27.1", features = ["derive"] }
-strum_macros = "0.27.1"
+strum = { version = "0.27.2", features = ["derive"] }
+strum_macros = "0.27.2"
aes-gcm = "0.10.3"
base64 = "0.22.1"
twox-hash = { version = "2.1.1", features = ["xxhash32"] }
@@ -160,8 +161,8 @@ tower-http = { version = "0.6.6", features = [
"trace",
] }
trait-variant = "0.1.2"
-webpki-roots = "1.0.1"
-zip = "4.2.0"
+webpki-roots = "1.0.2"
+zip = "4.3.0"
# Optional dependencies
mimalloc = "0.1"
diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md
index 85fb16c9..cf830ad0 100644
--- a/DEPENDENCIES.md
+++ b/DEPENDENCIES.md
@@ -39,7 +39,7 @@ arrayref: 0.3.9, "BSD-2-Clause",
arrayvec: 0.7.6, "Apache-2.0 OR MIT",
assert_cmd: 2.0.17, "Apache-2.0 OR MIT",
async-broadcast: 0.7.2, "Apache-2.0 OR MIT",
-async-compression: 0.4.25, "Apache-2.0 OR MIT",
+async-compression: 0.4.27, "Apache-2.0 OR MIT",
async-dropper: 0.3.1, "MIT",
async-dropper-derive: 0.3.1, "MIT",
async-dropper-simple: 0.2.6, "MIT",
@@ -56,8 +56,8 @@ atomic-waker: 1.1.2, "Apache-2.0 OR MIT",
attohttpc: 0.28.5, "MPL-2.0",
autocfg: 1.5.0, "Apache-2.0 OR MIT",
aws-creds: 0.38.0, "MIT",
-aws-lc-rs: 1.13.1, "ISC AND (Apache-2.0 OR ISC)",
-aws-lc-sys: 0.29.0, "ISC AND (Apache-2.0 OR ISC) AND OpenSSL",
+aws-lc-rs: 1.13.3, "ISC AND (Apache-2.0 OR ISC)",
+aws-lc-sys: 0.30.0, "ISC AND (Apache-2.0 OR ISC) AND OpenSSL",
aws-region: 0.26.0, "MIT",
axum: 0.7.9, "MIT",
axum: 0.8.4, "MIT",
@@ -86,15 +86,15 @@ bitvec: 1.0.1, "MIT",
blake3: 1.8.2, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR CC0-1.0",
block-buffer: 0.10.4, "Apache-2.0 OR MIT",
blowfish: 0.9.1, "Apache-2.0 OR MIT",
-bon: 3.6.4, "Apache-2.0 OR MIT",
-bon-macros: 3.6.4, "Apache-2.0 OR MIT",
+bon: 3.6.5, "Apache-2.0 OR MIT",
+bon-macros: 3.6.5, "Apache-2.0 OR MIT",
boolinator: 2.4.0, "Apache-2.0 OR MIT",
borsh: 1.5.7, "Apache-2.0 OR MIT",
borsh-derive: 1.5.7, "Apache-2.0",
brotli: 8.0.1, "BSD-3-Clause AND MIT",
brotli-decompressor: 5.0.0, "BSD-3-Clause OR MIT",
bstr: 1.12.0, "Apache-2.0 OR MIT",
-bumpalo: 3.18.1, "Apache-2.0 OR MIT",
+bumpalo: 3.19.0, "Apache-2.0 OR MIT",
byte-unit: 5.1.6, "MIT",
bytecheck: 0.6.12, "MIT",
bytecheck_derive: 0.6.12, "MIT",
@@ -103,14 +103,12 @@ bytemuck: 1.23.1, "Apache-2.0 OR MIT OR Zlib",
byteorder: 1.5.0, "MIT OR Unlicense",
bytes: 1.10.1, "MIT",
bytestring: 1.4.0, "Apache-2.0 OR MIT",
-bzip2: 0.5.2, "Apache-2.0 OR MIT",
bzip2: 0.6.0, "Apache-2.0 OR MIT",
-bzip2-sys: 0.1.13+1.0.8, "Apache-2.0 OR MIT",
camino: 1.1.10, "Apache-2.0 OR MIT",
cargo-platform: 0.1.9, "Apache-2.0 OR MIT",
cargo_metadata: 0.19.2, "MIT",
-castaway: 0.2.3, "MIT",
-cc: 1.2.27, "Apache-2.0 OR MIT",
+castaway: 0.2.4, "MIT",
+cc: 1.2.30, "Apache-2.0 OR MIT",
cesu8: 1.1.0, "Apache-2.0 OR MIT",
cexpr: 0.6.0, "Apache-2.0 OR MIT",
cfg-if: 1.0.1, "Apache-2.0 OR MIT",
@@ -120,20 +118,20 @@ charming_macros: 0.1.0, "Apache-2.0 OR MIT",
chrono: 0.4.41, "Apache-2.0 OR MIT",
cipher: 0.4.4, "Apache-2.0 OR MIT",
clang-sys: 1.8.1, "Apache-2.0",
-clap: 4.5.40, "Apache-2.0 OR MIT",
-clap_builder: 4.5.40, "Apache-2.0 OR MIT",
-clap_complete: 4.5.54, "Apache-2.0 OR MIT",
-clap_derive: 4.5.40, "Apache-2.0 OR MIT",
+clap: 4.5.41, "Apache-2.0 OR MIT",
+clap_builder: 4.5.41, "Apache-2.0 OR MIT",
+clap_complete: 4.5.55, "Apache-2.0 OR MIT",
+clap_derive: 4.5.41, "Apache-2.0 OR MIT",
clap_lex: 0.7.5, "Apache-2.0 OR MIT",
cmake: 0.1.54, "Apache-2.0 OR MIT",
-cobs: 0.2.3, "Apache-2.0 OR MIT",
+cobs: 0.3.0, "Apache-2.0 OR MIT",
colorchoice: 1.0.4, "Apache-2.0 OR MIT",
colored: 3.0.0, "MPL-2.0",
combine: 4.6.7, "MIT",
comfy-table: 7.1.4, "MIT",
compact_str: 0.7.1, "MIT",
concurrent-queue: 2.5.0, "Apache-2.0 OR MIT",
-config: 0.15.11, "Apache-2.0 OR MIT",
+config: 0.15.13, "Apache-2.0 OR MIT",
console: 0.15.11, "MIT",
console-api: 0.8.1, "MIT",
console-subscriber: 0.4.1, "MIT",
@@ -151,7 +149,7 @@ core-foundation-sys: 0.8.7, "Apache-2.0 OR MIT",
cpufeatures: 0.2.17, "Apache-2.0 OR MIT",
crc: 3.3.0, "Apache-2.0 OR MIT",
crc-catalog: 2.4.0, "Apache-2.0 OR MIT",
-crc32fast: 1.4.2, "Apache-2.0 OR MIT",
+crc32fast: 1.5.0, "Apache-2.0 OR MIT",
critical-section: 1.2.0, "Apache-2.0 OR MIT",
crossbeam: 0.8.4, "Apache-2.0 OR MIT",
crossbeam-channel: 0.5.15, "Apache-2.0 OR MIT",
@@ -161,7 +159,7 @@ crossbeam-queue: 0.3.12, "Apache-2.0 OR MIT",
crossbeam-utils: 0.8.21, "Apache-2.0 OR MIT",
crossterm: 0.28.1, "MIT",
crossterm_winapi: 0.9.1, "MIT",
-crunchy: 0.2.3, "MIT",
+crunchy: 0.2.4, "MIT",
crypto-common: 0.1.6, "Apache-2.0 OR MIT",
ctor: 0.4.2, "Apache-2.0 OR MIT",
ctor-proc-macro: 0.0.5, "Apache-2.0 OR MIT",
@@ -170,8 +168,11 @@ cucumber: 0.21.1, "Apache-2.0 OR MIT",
cucumber-codegen: 0.21.1, "Apache-2.0 OR MIT",
cucumber-expressions: 0.3.0, "Apache-2.0 OR MIT",
darling: 0.20.11, "MIT",
+darling: 0.21.0, "MIT",
darling_core: 0.20.11, "MIT",
+darling_core: 0.21.0, "MIT",
darling_macro: 0.20.11, "MIT",
+darling_macro: 0.21.0, "MIT",
dashmap: 6.1.0, "MIT",
dbus: 0.9.7, "Apache-2.0 OR MIT",
dbus-secret-service: 4.0.3, "Apache-2.0 OR MIT",
@@ -228,9 +229,8 @@ fastbloom: 0.9.0, "Apache-2.0 OR MIT",
fastrand: 2.3.0, "Apache-2.0 OR MIT",
figlet-rs: 0.1.5, "Apache-2.0",
figment: 0.10.19, "Apache-2.0 OR MIT",
-file-operation: 0.5.12, "MIT",
-filetime: 0.2.25, "Apache-2.0 OR MIT",
-flatbuffers: 24.12.23, "Apache-2.0",
+file-operation: 0.5.16, "MIT",
+flatbuffers: 25.2.10, "Apache-2.0",
flate2: 1.1.2, "Apache-2.0 OR MIT",
float-cmp: 0.10.0, "MIT",
flume: 0.11.1, "Apache-2.0 OR MIT",
@@ -296,8 +296,8 @@ gloo-worker: 0.4.0, "Apache-2.0 OR MIT",
gloo-worker: 0.5.0, "Apache-2.0 OR MIT",
gloo-worker-macros: 0.1.0, "Apache-2.0 OR MIT",
governor: 0.10.0, "MIT",
-h2: 0.3.26, "MIT",
-h2: 0.4.10, "MIT",
+h2: 0.3.27, "MIT",
+h2: 0.4.11, "MIT",
halfbrown: 0.3.0, "Apache-2.0 OR MIT",
handlebars: 6.3.2, "MIT",
hash32: 0.2.1, "Apache-2.0 OR MIT",
@@ -327,7 +327,7 @@ humantime: 2.2.0, "Apache-2.0 OR MIT",
hyper: 1.6.0, "MIT",
hyper-rustls: 0.27.7, "Apache-2.0 OR ISC OR MIT",
hyper-timeout: 0.5.2, "Apache-2.0 OR MIT",
-hyper-util: 0.1.14, "MIT",
+hyper-util: 0.1.16, "MIT",
iana-time-zone: 0.1.63, "Apache-2.0 OR MIT",
iana-time-zone-haiku: 0.1.2, "Apache-2.0 OR MIT",
icu_collections: 2.0.0, "Unicode-3.0",
@@ -344,6 +344,7 @@ iggy: 0.7.0, "Apache-2.0",
iggy-bench: 0.2.4, "Apache-2.0",
iggy-cli: 0.9.0, "Apache-2.0",
iggy-connectors: 0.1.0, "Apache-2.0",
+iggy-mcp: 0.1.0, "N/A",
iggy_binary_protocol: 0.7.0, "Apache-2.0",
iggy_common: 0.7.0, "Apache-2.0",
iggy_connector_postgres_sink: 0.1.0, "Apache-2.0",
@@ -358,7 +359,7 @@ impl-more: 0.1.9, "Apache-2.0 OR MIT",
implicit-clone: 0.4.9, "Apache-2.0 OR MIT",
implicit-clone-derive: 0.1.2, "Apache-2.0 OR MIT",
indexmap: 1.9.3, "Apache-2.0 OR MIT",
-indexmap: 2.9.0, "Apache-2.0 OR MIT",
+indexmap: 2.10.0, "Apache-2.0 OR MIT",
indices: 0.3.6, "Apache-2.0",
inflections: 1.1.1, "MIT",
inlinable_string: 0.1.15, "Apache-2.0 OR MIT",
@@ -368,6 +369,7 @@ inout: 0.1.4, "Apache-2.0 OR MIT",
instant: 0.1.13, "BSD-3-Clause",
integration: 0.0.1, "Apache-2.0",
inventory: 0.3.20, "Apache-2.0 OR MIT",
+io-uring: 0.7.9, "Apache-2.0 OR MIT",
ipnet: 2.11.0, "Apache-2.0 OR MIT",
iri-string: 0.7.8, "Apache-2.0 OR MIT",
is_terminal_polyfill: 1.70.1, "Apache-2.0 OR MIT",
@@ -403,7 +405,7 @@ liblzma: 0.4.2, "Apache-2.0 OR MIT",
liblzma-sys: 0.4.4, "Apache-2.0 OR MIT",
libm: 0.2.15, "MIT",
libmimalloc-sys: 0.1.43, "MIT",
-libredox: 0.1.3, "MIT",
+libredox: 0.1.6, "MIT",
libsqlite3-sys: 0.30.1, "MIT",
libyml: 0.0.5, "MIT",
libz-rs-sys: 0.5.1, "Zlib",
@@ -450,7 +452,7 @@ nom_locate: 4.2.0, "MIT",
nonzero_ext: 0.3.0, "Apache-2.0",
nonzero_lit: 0.1.2, "Apache-2.0 OR CC0-1.0 OR MIT",
normalize-line-endings: 0.3.0, "Apache-2.0",
-notify: 8.0.0, "CC0-1.0",
+notify: 8.1.0, "CC0-1.0",
notify-types: 2.0.0, "Apache-2.0 OR MIT",
nougat: 0.2.4, "Apache-2.0 OR MIT OR Zlib",
nougat-proc_macros: 0.2.4, "Apache-2.0 OR MIT OR Zlib",
@@ -479,7 +481,7 @@ opaque-debug: 0.3.1, "Apache-2.0 OR MIT",
openssl: 0.10.73, "Apache-2.0",
openssl-macros: 0.1.1, "Apache-2.0 OR MIT",
openssl-probe: 0.1.6, "Apache-2.0 OR MIT",
-openssl-src: 300.5.0+3.5.0, "Apache-2.0 OR MIT",
+openssl-src: 300.5.1+3.5.1, "Apache-2.0 OR MIT",
openssl-sys: 0.9.109, "MIT",
opentelemetry: 0.30.0, "Apache-2.0",
opentelemetry-appender-tracing: 0.30.1, "Apache-2.0",
@@ -524,9 +526,10 @@ polonius-the-crab: 0.2.1, "Apache-2.0 OR MIT OR Zlib",
polyval: 0.6.2, "Apache-2.0 OR MIT",
portable-atomic: 1.11.1, "Apache-2.0 OR MIT",
portable-atomic-util: 0.2.4, "Apache-2.0 OR MIT",
-postcard: 1.1.1, "Apache-2.0 OR MIT",
+postcard: 1.1.2, "Apache-2.0 OR MIT",
potential_utf: 0.1.2, "Unicode-3.0",
powerfmt: 0.2.0, "Apache-2.0 OR MIT",
+ppmd-rust: 1.2.1, "CC0-1.0",
ppv-lite86: 0.2.21, "Apache-2.0 OR MIT",
predicates: 3.1.3, "Apache-2.0 OR MIT",
predicates-core: 1.0.9, "Apache-2.0 OR MIT",
@@ -542,11 +545,14 @@ prokio: 0.1.0, "Apache-2.0 OR MIT",
prometheus-client: 0.23.1, "Apache-2.0 OR MIT",
prometheus-client-derive-encode: 0.4.2, "Apache-2.0 OR MIT",
prost: 0.13.5, "Apache-2.0",
+prost: 0.14.1, "Apache-2.0",
prost-derive: 0.13.5, "Apache-2.0",
-prost-reflect: 0.15.3, "Apache-2.0 OR MIT",
+prost-derive: 0.14.1, "Apache-2.0",
+prost-reflect: 0.16.0, "Apache-2.0 OR MIT",
prost-types: 0.13.5, "Apache-2.0",
-protox: 0.8.0, "Apache-2.0 OR MIT",
-protox-parse: 0.8.0, "Apache-2.0 OR MIT",
+prost-types: 0.14.1, "Apache-2.0",
+protox: 0.9.0, "Apache-2.0 OR MIT",
+protox-parse: 0.9.0, "Apache-2.0 OR MIT",
ptr_meta: 0.1.4, "MIT",
ptr_meta_derive: 0.1.4, "MIT",
quanta: 0.12.6, "MIT",
@@ -559,7 +565,7 @@ quote: 1.0.40, "Apache-2.0 OR MIT",
r-efi: 5.3.0, "Apache-2.0 OR LGPL-2.1-or-later OR MIT",
radium: 0.7.0, "MIT",
rand: 0.8.5, "Apache-2.0 OR MIT",
-rand: 0.9.1, "Apache-2.0 OR MIT",
+rand: 0.9.2, "Apache-2.0 OR MIT",
rand_chacha: 0.3.1, "Apache-2.0 OR MIT",
rand_chacha: 0.9.0, "Apache-2.0 OR MIT",
rand_core: 0.6.4, "Apache-2.0 OR MIT",
@@ -567,9 +573,9 @@ rand_core: 0.9.3, "Apache-2.0 OR MIT",
raw-cpuid: 11.5.0, "MIT",
rayon: 1.10.0, "Apache-2.0 OR MIT",
rayon-core: 1.12.1, "Apache-2.0 OR MIT",
-rcgen: 0.13.2, "Apache-2.0 OR MIT",
+rcgen: 0.14.3, "Apache-2.0 OR MIT",
redox_syscall: 0.2.16, "MIT",
-redox_syscall: 0.5.13, "MIT",
+redox_syscall: 0.5.15, "MIT",
redox_users: 0.5.0, "MIT",
ref-cast: 1.0.24, "Apache-2.0 OR MIT",
ref-cast-impl: 1.0.24, "Apache-2.0 OR MIT",
@@ -581,17 +587,19 @@ regex-syntax: 0.6.29, "Apache-2.0 OR MIT",
regex-syntax: 0.7.5, "Apache-2.0 OR MIT",
regex-syntax: 0.8.5, "Apache-2.0 OR MIT",
rend: 0.4.2, "MIT",
-reqwest: 0.12.20, "Apache-2.0 OR MIT",
+reqwest: 0.12.22, "Apache-2.0 OR MIT",
reqwest-middleware: 0.4.2, "Apache-2.0 OR MIT",
reqwest-retry: 0.7.0, "Apache-2.0 OR MIT",
retry-policies: 0.4.0, "Apache-2.0 OR MIT",
ring: 0.17.14, "Apache-2.0 AND ISC",
rkyv: 0.7.45, "MIT",
rkyv_derive: 0.7.45, "MIT",
+rmcp: 0.3.0, "Apache-2.0 OR MIT",
+rmcp-macros: 0.3.0, "Apache-2.0 OR MIT",
ron: 0.8.1, "Apache-2.0 OR MIT",
route-recognizer: 0.3.1, "MIT",
rsa: 0.9.8, "Apache-2.0 OR MIT",
-rust-ini: 0.21.1, "MIT",
+rust-ini: 0.21.2, "MIT",
rust-s3: 0.36.0-beta.2, "MIT",
rust_decimal: 1.37.2, "MIT",
rustc-demangle: 0.1.25, "Apache-2.0 OR MIT",
@@ -599,14 +607,14 @@ rustc-hash: 1.1.0, "Apache-2.0 OR MIT",
rustc-hash: 2.1.1, "Apache-2.0 OR MIT",
rustc_version: 0.4.1, "Apache-2.0 OR MIT",
rustix: 0.38.44, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
-rustix: 1.0.7, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
-rustls: 0.23.28, "Apache-2.0 OR ISC OR MIT",
+rustix: 1.0.8, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
+rustls: 0.23.29, "Apache-2.0 OR ISC OR MIT",
rustls-native-certs: 0.8.1, "Apache-2.0 OR ISC OR MIT",
rustls-pemfile: 2.2.0, "Apache-2.0 OR ISC OR MIT",
rustls-pki-types: 1.12.0, "Apache-2.0 OR MIT",
rustls-platform-verifier: 0.5.3, "Apache-2.0 OR MIT",
rustls-platform-verifier-android: 0.1.1, "Apache-2.0 OR MIT",
-rustls-webpki: 0.103.3, "ISC",
+rustls-webpki: 0.103.4, "ISC",
rustversion: 1.0.21, "Apache-2.0 OR MIT",
rxml: 0.11.1, "MIT",
rxml_validation: 0.11.0, "MIT",
@@ -616,9 +624,11 @@ same-file: 1.0.6, "MIT OR Unlicense",
scc: 2.3.4, "Apache-2.0",
schannel: 0.1.27, "MIT",
schemars: 0.9.0, "MIT",
+schemars: 1.0.4, "MIT",
+schemars_derive: 1.0.4, "MIT",
scoped-tls: 1.0.1, "Apache-2.0 OR MIT",
scopeguard: 1.2.0, "Apache-2.0 OR MIT",
-sdd: 3.0.8, "Apache-2.0",
+sdd: 3.0.10, "Apache-2.0",
seahash: 4.1.0, "MIT",
sealed: 0.5.0, "Apache-2.0 OR MIT",
secrecy: 0.10.3, "Apache-2.0 OR MIT",
@@ -629,12 +639,14 @@ serde: 1.0.219, "Apache-2.0 OR MIT",
serde-wasm-bindgen: 0.5.0, "MIT",
serde-wasm-bindgen: 0.6.5, "MIT",
serde_derive: 1.0.219, "Apache-2.0 OR MIT",
-serde_json: 1.0.140, "Apache-2.0 OR MIT",
+serde_derive_internals: 0.29.1, "Apache-2.0 OR MIT",
+serde_json: 1.0.141, "Apache-2.0 OR MIT",
serde_path_to_error: 0.1.17, "Apache-2.0 OR MIT",
serde_spanned: 0.6.9, "Apache-2.0 OR MIT",
+serde_spanned: 1.0.0, "Apache-2.0 OR MIT",
serde_urlencoded: 0.7.1, "Apache-2.0 OR MIT",
-serde_with: 3.13.0, "Apache-2.0 OR MIT",
-serde_with_macros: 3.13.0, "Apache-2.0 OR MIT",
+serde_with: 3.14.0, "Apache-2.0 OR MIT",
+serde_with_macros: 3.14.0, "Apache-2.0 OR MIT",
serde_yml: 0.0.12, "Apache-2.0 OR MIT",
serial_test: 3.2.0, "MIT",
serial_test_derive: 3.2.0, "MIT",
@@ -657,6 +669,7 @@ smawk: 0.3.2, "MIT",
snafu: 0.8.6, "Apache-2.0 OR MIT",
snafu-derive: 0.8.6, "Apache-2.0 OR MIT",
socket2: 0.5.10, "Apache-2.0 OR MIT",
+socket2: 0.6.0, "Apache-2.0 OR MIT",
spin: 0.9.8, "MIT",
spinning_top: 0.3.0, "Apache-2.0 OR MIT",
spki: 0.7.3, "Apache-2.0 OR MIT",
@@ -667,13 +680,14 @@ sqlx-macros-core: 0.8.6, "Apache-2.0 OR MIT",
sqlx-mysql: 0.8.6, "Apache-2.0 OR MIT",
sqlx-postgres: 0.8.6, "Apache-2.0 OR MIT",
sqlx-sqlite: 0.8.6, "Apache-2.0 OR MIT",
+sse-stream: 0.2.1, "Apache-2.0 OR MIT",
stable_deref_trait: 1.2.0, "Apache-2.0 OR MIT",
static-toml: 1.3.0, "MIT",
static_assertions: 1.1.0, "Apache-2.0 OR MIT",
stringprep: 0.1.5, "Apache-2.0 OR MIT",
strsim: 0.11.1, "MIT",
-strum: 0.27.1, "MIT",
-strum_macros: 0.27.1, "MIT",
+strum: 0.27.2, "MIT",
+strum_macros: 0.27.2, "MIT",
subtle: 2.6.1, "BSD-3-Clause",
syn: 1.0.109, "Apache-2.0 OR MIT",
syn: 2.0.104, "Apache-2.0 OR MIT",
@@ -683,7 +697,7 @@ synthez: 0.3.1, "BlueOak-1.0.0",
synthez-codegen: 0.3.1, "BlueOak-1.0.0",
synthez-core: 0.3.1, "BlueOak-1.0.0",
sysinfo: 0.34.2, "MIT",
-sysinfo: 0.35.2, "MIT",
+sysinfo: 0.36.1, "MIT",
tagptr: 0.2.0, "Apache-2.0 OR MIT",
tap: 1.0.1, "MIT",
tempfile: 3.20.0, "Apache-2.0 OR MIT",
@@ -705,16 +719,20 @@ tiny-keccak: 2.0.2, "CC0-1.0",
tinystr: 0.8.1, "Unicode-3.0",
tinyvec: 1.9.0, "Apache-2.0 OR MIT OR Zlib",
tinyvec_macros: 0.1.1, "Apache-2.0 OR MIT OR Zlib",
-tokio: 1.45.1, "MIT",
+tokio: 1.46.1, "MIT",
tokio-macros: 2.5.0, "MIT",
tokio-rustls: 0.26.2, "Apache-2.0 OR MIT",
tokio-stream: 0.1.17, "MIT",
tokio-util: 0.7.15, "MIT",
toml: 0.8.23, "Apache-2.0 OR MIT",
+toml: 0.9.2, "Apache-2.0 OR MIT",
toml_datetime: 0.6.11, "Apache-2.0 OR MIT",
+toml_datetime: 0.7.0, "Apache-2.0 OR MIT",
toml_edit: 0.19.15, "Apache-2.0 OR MIT",
toml_edit: 0.22.27, "Apache-2.0 OR MIT",
+toml_parser: 1.0.1, "Apache-2.0 OR MIT",
toml_write: 0.1.2, "Apache-2.0 OR MIT",
+toml_writer: 1.0.2, "Apache-2.0 OR MIT",
tonic: 0.12.3, "MIT",
tonic: 0.13.1, "MIT",
tools: 0.1.0, "Apache-2.0",
@@ -731,7 +749,6 @@ tracing-log: 0.2.0, "MIT",
tracing-opentelemetry: 0.31.0, "MIT",
tracing-subscriber: 0.3.19, "MIT",
trait-variant: 0.1.2, "Apache-2.0 OR MIT",
-trim-in-place: 0.1.7, "MIT",
try-lock: 0.2.5, "MIT",
twox-hash: 2.1.1, "MIT",
typed-builder: 0.15.2, "Apache-2.0 OR MIT",
@@ -785,9 +802,9 @@ wasm-timer: 0.2.5, "MIT",
web-sys: 0.3.77, "Apache-2.0 OR MIT",
web-time: 1.1.0, "Apache-2.0 OR MIT",
webpki-root-certs: 0.26.11, "CDLA-Permissive-2.0",
-webpki-root-certs: 1.0.1, "CDLA-Permissive-2.0",
+webpki-root-certs: 1.0.2, "CDLA-Permissive-2.0",
webpki-roots: 0.26.11, "CDLA-Permissive-2.0",
-webpki-roots: 1.0.1, "CDLA-Permissive-2.0",
+webpki-roots: 1.0.2, "CDLA-Permissive-2.0",
which: 4.4.2, "MIT",
whoami: 1.6.0, "Apache-2.0 OR BSL-1.0 OR MIT",
wide: 0.7.33, "Apache-2.0 OR MIT OR Zlib",
@@ -851,7 +868,7 @@ windows_x86_64_msvc: 0.48.5, "Apache-2.0 OR MIT",
windows_x86_64_msvc: 0.52.6, "Apache-2.0 OR MIT",
windows_x86_64_msvc: 0.53.0, "Apache-2.0 OR MIT",
winnow: 0.5.40, "MIT",
-winnow: 0.7.11, "MIT",
+winnow: 0.7.12, "MIT",
wit-bindgen-rt: 0.39.0, "Apache-2.0 OR Apache-2.0 WITH LLVM-exception OR MIT",
writeable: 0.6.1, "Unicode-3.0",
wyz: 0.5.1, "MIT",
@@ -874,7 +891,7 @@ zeroize_derive: 1.4.2, "Apache-2.0 OR MIT",
zerotrie: 0.2.2, "Unicode-3.0",
zerovec: 0.11.2, "Unicode-3.0",
zerovec-derive: 0.11.1, "Unicode-3.0",
-zip: 4.2.0, "MIT",
+zip: 4.3.0, "MIT",
zlib-rs: 0.5.1, "Zlib",
zopfli: 0.8.2, "Apache-2.0",
zstd: 0.13.3, "MIT",
diff --git a/Dockerfile.ci.dockerignore b/Dockerfile.ci.dockerignore
index 251cad50..e2fdd5f0 100644
--- a/Dockerfile.ci.dockerignore
+++ b/Dockerfile.ci.dockerignore
@@ -19,7 +19,10 @@
.github
/assets
/local_data
+/licenses
/certs
+/helm
+/web
Dockerfile
docker-compose.yml
.dockerignore
diff --git a/docker-compose.yml b/Dockerfile.connectors.ci
similarity index 68%
copy from docker-compose.yml
copy to Dockerfile.connectors.ci
index 347bef1b..b195c914 100644
--- a/docker-compose.yml
+++ b/Dockerfile.connectors.ci
@@ -15,26 +15,18 @@
# specific language governing permissions and limitations
# under the License.
-version: '3.5'
-services:
+FROM alpine:latest
- iggy-server:
- build: .
- container_name: iggy-server
- restart: unless-stopped
- networks:
- - iggy
- ports:
- - 3000:3000
- - 8080:8080
- - 8090:8090
- volumes:
- - iggy-server:/local_data
+ARG IGGY_CONNECTORS_PATH
+RUN test -n "$IGGY_CONNECTORS_PATH" || (echo "IGGY_CONNECTORS_PATH not set" && false)
-volumes:
- iggy-server:
- driver: local
+WORKDIR /iggy
-networks:
- iggy:
- name: iggy-network
+COPY ./core/connectors/runtime/config.toml ./config.toml
+COPY ${IGGY_CONNECTORS_PATH} ./
+
+RUN chmod +x ./iggy-connectors
+
+RUN ln -sf /iggy/iggy-connectors /usr/bin/iggy-connectors
+
+CMD ["/iggy/iggy-connectors"]
diff --git a/Dockerfile.ci.dockerignore b/Dockerfile.connectors.ci.dockerignore
similarity index 97%
copy from Dockerfile.ci.dockerignore
copy to Dockerfile.connectors.ci.dockerignore
index 251cad50..e2fdd5f0 100644
--- a/Dockerfile.ci.dockerignore
+++ b/Dockerfile.connectors.ci.dockerignore
@@ -19,7 +19,10 @@
.github
/assets
/local_data
+/licenses
/certs
+/helm
+/web
Dockerfile
docker-compose.yml
.dockerignore
diff --git a/Dockerfile.ci.dockerignore b/Dockerfile.mcp.ci
similarity index 73%
copy from Dockerfile.ci.dockerignore
copy to Dockerfile.mcp.ci
index 251cad50..bc73ebe0 100644
--- a/Dockerfile.ci.dockerignore
+++ b/Dockerfile.mcp.ci
@@ -15,13 +15,18 @@
# specific language governing permissions and limitations
# under the License.
-.config
-.github
-/assets
-/local_data
-/certs
-Dockerfile
-docker-compose.yml
-.dockerignore
-.git
-.gitignore
+FROM alpine:latest
+
+ARG IGGY_MCP_PATH
+RUN test -n "$IGGY_MCP_PATH" || (echo "IGGY_MCP_PATH not set" && false)
+
+WORKDIR /iggy
+
+COPY ./core/ai/mcp/config.toml ./config.toml
+COPY ${IGGY_MCP_PATH} ./
+
+RUN chmod +x ./iggy-mcp
+
+RUN ln -sf /iggy/iggy-mcp /usr/bin/iggy-mcp
+
+CMD ["/iggy/iggy-mcp"]
diff --git a/Dockerfile.ci.dockerignore b/Dockerfile.mcp.ci.dockerignore
similarity index 97%
copy from Dockerfile.ci.dockerignore
copy to Dockerfile.mcp.ci.dockerignore
index 251cad50..e2fdd5f0 100644
--- a/Dockerfile.ci.dockerignore
+++ b/Dockerfile.mcp.ci.dockerignore
@@ -19,7 +19,10 @@
.github
/assets
/local_data
+/licenses
/certs
+/helm
+/web
Dockerfile
docker-compose.yml
.dockerignore
diff --git a/core/ai/README.md b/core/ai/README.md
new file mode 100644
index 00000000..c87875ab
--- /dev/null
+++ b/core/ai/README.md
@@ -0,0 +1,5 @@
+# Apache Iggy AI
+
+This module contains the AI-related components of the Apache Iggy message streaming infrastructure.
+
+Please check the **[MCP Server](https://github.com/apache/iggy/tree/master/core/ai/mcp)** for the details about Iggy MCP (Model Context Protocol) server.
diff --git a/core/ai/mcp/Cargo.toml b/core/ai/mcp/Cargo.toml
new file mode 100644
index 00000000..a8674373
--- /dev/null
+++ b/core/ai/mcp/Cargo.toml
@@ -0,0 +1,25 @@
+[package]
+name = "iggy-mcp"
+version = "0.1.0"
+edition = "2024"
+
+[dependencies]
+axum = { workspace = true }
+axum-server = { workspace = true }
+config = { workspace = true }
+dotenvy = { workspace = true }
+figlet-rs = { workspace = true }
+iggy = { workspace = true }
+rmcp = { version = "0.3.0", features = [
+ "server",
+ "transport-io",
+ "transport-sse-server",
+ "transport-streamable-http-server",
+] }
+serde = { workspace = true }
+strum = { workspace = true }
+thiserror = { workspace = true }
+tokio = { workspace = true }
+tower-http = { workspace = true }
+tracing = { workspace = true }
+tracing-subscriber = { workspace = true }
diff --git a/docker-compose.yml b/core/ai/mcp/Dockerfile
similarity index 68%
copy from docker-compose.yml
copy to core/ai/mcp/Dockerfile
index 347bef1b..8422eedc 100644
--- a/docker-compose.yml
+++ b/core/ai/mcp/Dockerfile
@@ -15,26 +15,17 @@
# specific language governing permissions and limitations
# under the License.
-version: '3.5'
-services:
+FROM rust:latest AS builder
+WORKDIR /build
+COPY . /build
+RUN cargo build --bin iggy-mcp --release
- iggy-server:
- build: .
- container_name: iggy-server
- restart: unless-stopped
- networks:
- - iggy
- ports:
- - 3000:3000
- - 8080:8080
- - 8090:8090
- volumes:
- - iggy-server:/local_data
+FROM debian:bookworm-slim
+RUN apt-get update && apt-get install -y \
+ ca-certificates \
+ liblzma5 \
+ && rm -rf /var/lib/apt/lists/*
+COPY ./core/ai/mcp/config.toml ./config.toml
+COPY --from=builder /build/target/release/iggy-mcp .
-volumes:
- iggy-server:
- driver: local
-
-networks:
- iggy:
- name: iggy-network
+CMD ["/iggy-mcp"]
diff --git a/core/ai/mcp/README.md b/core/ai/mcp/README.md
new file mode 100644
index 00000000..33459f16
--- /dev/null
+++ b/core/ai/mcp/README.md
@@ -0,0 +1,62 @@
+# Apache Iggy MCP Server
+
+The [Model Context Protocol](https://modelcontextprotocol.io) (MCP) is an open protocol that standardizes how applications provide context to LLMs. The Apache Iggy MCP Server is an implementation of the MCP protocol for the message streaming infrastructure.
+
+To start the MCP server, simply run `cargo run --bin iggy-mcp`.
+
+The minimal viable configuration requires at least the Iggy credentials, to create the connection with the running Iggy server using TCP with which the MCP server will communicate. You can choose between HTTP and STDIO transports (e.g. for the local usage with tools such as [Claude Desktop](https://claude.ai/download) choose `stdio`).
+
+```toml
+transport = "stdio" # http or stdio are supported
+
+[iggy]
+address = "localhost:8090" # TCP address of the Iggy server
+username = "iggy"
+password = "iggy"
+# token = "secret" # Personal Access Token (PAT) can be used instead of username and password
+# consumer = "iggy-mcp" # Optional consumer name
+
+[http] # Optional HTTP API configuration
+address = "127.0.0.1:8082"
+path = "/mcp"
+
+[http.cors] # Optional CORS configuration for HTTP API
+enabled = false
+allowed_methods = ["GET", "POST", "PUT", "DELETE"]
+allowed_origins = ["*"]
+allowed_headers = ["content-type"]
+exposed_headers = [""]
+allow_credentials = false
+allow_private_network = false
+
+[http.tls] # Optional TLS configuration for HTTP API
+enabled = false
+cert = "core/certs/iggy_cert.pem"
+key = "core/certs/iggy_key.pem"
+
+[permissions]
+create = true
+read = true
+update = true
+delete = true
+```
+
+Keep in mind that either of `toml`, `yaml`, or `json` formats are supported for the configuration file. The path to the configuration can be overriden by `IGGY_MCP_CONFIG_PATH` environment variable. Each configuration section can be also additionally updated by using the following convention `IGGY_MCP_SECTION_NAME.KEY_NAME` e.g. `IGGY_MCP_IGGY_USERNAME` and so on.
+
+Here's the example configuration to be used with Claude Desktop;
+
+```json
+{
+ "mcpServers": {
+ "iggy": {
+ "command": "/path/to/iggy-mcp",
+ "args": [],
+ "env": {
+ "IGGY_MCP_TRANSPORT": "stdio"
+ }
+ }
+ }
+}
+```
+
+Remember to use the appropriate Iggy account credentials for your environment (e.g. create the user with read-only permissions to avoid modifying the data). On top of this, you can also configure the `permissions` for the MCP server to control which operations are allowed (this will be checked first, before forwarding the actual request to the Iggy server.)
diff --git a/core/ai/mcp/config.toml b/core/ai/mcp/config.toml
new file mode 100644
index 00000000..357502cd
--- /dev/null
+++ b/core/ai/mcp/config.toml
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+transport = "http" # http or stdio are supported
+
+[http] # Optional HTTP API configuration
+address = "127.0.0.1:8082"
+path = "/mcp"
+
+[http.cors] # Optional CORS configuration for HTTP API
+enabled = false
+allowed_methods = ["GET", "POST", "PUT", "DELETE"]
+allowed_origins = ["*"]
+allowed_headers = ["content-type"]
+exposed_headers = [""]
+allow_credentials = false
+allow_private_network = false
+
+[http.tls] # Optional TLS configuration for HTTP API
+enabled = false
+cert = "core/certs/iggy_cert.pem"
+key = "core/certs/iggy_key.pem"
+
+[iggy]
+address = "localhost:8090"
+username = "iggy"
+password = "iggy"
+# token = "secret" # Personal Access Token (PAT) can be used instead of username and password
+# consumer = "iggy-mcp" # Optional consumer name
+
+[permissions]
+create = true
+read = true
+update = true
+delete = true
diff --git a/core/connectors/runtime/src/api/mod.rs b/core/ai/mcp/src/api.rs
similarity index 55%
copy from core/connectors/runtime/src/api/mod.rs
copy to core/ai/mcp/src/api.rs
index b6987d1d..7008927d 100644
--- a/core/connectors/runtime/src/api/mod.rs
+++ b/core/ai/mcp/src/api.rs
@@ -16,43 +16,60 @@
* under the License.
*/
-use crate::context::RuntimeContext;
-use auth::resolve_api_key;
-use axum::{Json, Router, middleware, routing::get};
+use crate::{
+ Permissions,
+ configs::{HttpApiConfig, configure_cors},
+ error::McpRuntimeError,
+ service::IggyService,
+};
+use axum::{Json, Router, routing::get};
use axum_server::tls_rustls::RustlsConfig;
-use config::{HttpApiConfig, configure_cors};
+use iggy::prelude::{Consumer, IggyClient};
+use rmcp::{
+ serde_json,
+ transport::{
+ StreamableHttpService, streamable_http_server::session::local::LocalSessionManager,
+ },
+};
use std::{net::SocketAddr, path::PathBuf, sync::Arc};
use tokio::spawn;
use tracing::{error, info};
-mod auth;
-pub mod config;
-mod error;
-mod models;
-mod sink;
-mod source;
+pub async fn init(
+ config: HttpApiConfig,
+ iggy_client: Arc<IggyClient>,
+ iggy_consumer: Arc<Consumer>,
+ permissions: Permissions,
+) -> Result<(), McpRuntimeError> {
+ let service = StreamableHttpService::new(
+ move || {
+ Ok(IggyService::new(
+ iggy_client.clone(),
+ iggy_consumer.clone(),
+ permissions,
+ ))
+ },
+ LocalSessionManager::default().into(),
+ Default::default(),
+ );
-const NAME: &str = env!("CARGO_PKG_NAME");
+ if !config.path.starts_with("/") {
+ error!("HTTP API path must start with '/'");
+ return Err(McpRuntimeError::InvalidApiPath);
+ }
-pub async fn init(config: &HttpApiConfig, context: Arc<RuntimeContext>) {
- if !config.enabled {
- info!("{NAME} HTTP API is disabled");
- return;
+ if config.path == "/" {
+ error!("HTTP API path cannot be '/'");
+ return Err(McpRuntimeError::InvalidApiPath);
}
let mut app = Router::new()
- .route("/", get(|| async { "Connector Runtime API" }))
+ .route("/", get(|| async { "Iggy MCP Server" }))
.route(
"/health",
get(|| async { Json(serde_json::json!({ "status": "healthy" })) }),
)
- .merge(sink::router(context.clone()))
- .merge(source::router(context.clone()));
-
- app = app.layer(middleware::from_fn_with_state(
- context.clone(),
- resolve_api_key,
- ));
+ .nest_service(&config.path, service);
if let Some(cors) = &config.cors {
if cors.enabled {
@@ -65,14 +82,21 @@ pub async fn init(config: &HttpApiConfig, context: Arc<RuntimeContext>) {
.as_ref()
.map(|tls| tls.enabled)
.unwrap_or_default();
+
if !tls_enabled {
let listener = tokio::net::TcpListener::bind(&config.address)
.await
- .unwrap_or_else(|_| panic!("Failed to bind to HTTP address {}", config.address));
+ .map_err(|error| {
+ error!("Failed to bind TCP listener: {:?}", error);
+ McpRuntimeError::FailedToStartHttpServer
+ })?;
let address = listener
.local_addr()
.expect("Failed to get local address for HTTP server");
- info!("Started {NAME} on: {address}");
+ info!(
+ "HTTP API listening on: {address}, MCP path: {}",
+ config.path
+ );
spawn(async move {
if let Err(error) = axum::serve(
listener,
@@ -80,31 +104,35 @@ pub async fn init(config: &HttpApiConfig, context: Arc<RuntimeContext>) {
)
.await
{
- error!("Failed to start {NAME} server, error {}", error);
+ error!("Failed to start MCP server, error: {error}");
}
});
- return;
+ return Ok(());
}
let tls = config.tls.as_ref().expect("TLS configuration is required");
- let tls_config =
- RustlsConfig::from_pem_file(PathBuf::from(&tls.cert_file), PathBuf::from(&tls.key_file))
- .await
- .unwrap();
+ let tls_config = RustlsConfig::from_pem_file(PathBuf::from(&tls.cert), PathBuf::from(&tls.key))
+ .await
+ .unwrap();
let listener = std::net::TcpListener::bind(&config.address).unwrap();
let address = listener
.local_addr()
.expect("Failed to get local address for HTTPS / TLS server");
- info!("Started {NAME} on: {address}");
+ info!(
+ "HTTP API (TLS) listening on: {address}, MCP path: {}",
+ config.path
+ );
spawn(async move {
if let Err(error) = axum_server::from_tcp_rustls(listener, tls_config)
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.await
{
- error!("Failed to start {NAME} server, error: {}", error);
+ error!("Failed to start MCP server, error: {error}");
}
});
+
+ Ok(())
}
diff --git a/core/ai/mcp/src/configs.rs b/core/ai/mcp/src/configs.rs
new file mode 100644
index 00000000..332910ed
--- /dev/null
+++ b/core/ai/mcp/src/configs.rs
@@ -0,0 +1,178 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use axum::http::Method;
+use iggy::prelude::{DEFAULT_ROOT_PASSWORD, DEFAULT_ROOT_USERNAME};
+use serde::{Deserialize, Serialize};
+use strum::Display;
+use tower_http::cors::{AllowOrigin, CorsLayer};
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct McpServerConfig {
+ pub http: Option<HttpApiConfig>,
+ pub iggy: IggyConfig,
+ pub permissions: PermissionsConfig,
+ pub transport: McpTransport,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct IggyConfig {
+ pub address: String,
+ pub username: Option<String>,
+ pub password: Option<String>,
+ pub token: Option<String>,
+ pub consumer: Option<String>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct HttpApiConfig {
+ pub address: String,
+ pub path: String,
+ pub cors: Option<HttpCorsConfig>,
+ pub tls: Option<HttpTlsConfig>,
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize)]
+pub struct PermissionsConfig {
+ pub create: bool,
+ pub read: bool,
+ pub update: bool,
+ pub delete: bool,
+}
+
+#[derive(Debug, Default, Deserialize, Serialize, Clone)]
+pub struct HttpTlsConfig {
+ pub enabled: bool,
+ pub cert: String,
+ pub key: String,
+}
+
+#[derive(Debug, Default, Deserialize, Serialize, Clone)]
+pub struct HttpCorsConfig {
+ pub enabled: bool,
+ pub allowed_methods: Vec<String>,
+ pub allowed_origins: Vec<String>,
+ pub allowed_headers: Vec<String>,
+ pub exposed_headers: Vec<String>,
+ pub allow_credentials: bool,
+ pub allow_private_network: bool,
+}
+
+#[derive(Clone, Copy, Debug, Default, Display, PartialEq, Eq, Serialize, Deserialize)]
+#[strum(serialize_all = "snake_case")]
+#[serde(rename_all = "snake_case")]
+pub enum McpTransport {
+ #[default]
+ #[strum(to_string = "http")]
+ Http,
+ #[strum(to_string = "stdio")]
+ Stdio,
+}
+
+impl Default for McpServerConfig {
+ fn default() -> Self {
+ Self {
+ http: Some(HttpApiConfig::default()),
+ iggy: IggyConfig::default(),
+ permissions: PermissionsConfig::default(),
+ transport: McpTransport::Http,
+ }
+ }
+}
+
+impl Default for IggyConfig {
+ fn default() -> Self {
+ Self {
+ address: "localhost:8090".to_owned(),
+ username: Some(DEFAULT_ROOT_USERNAME.to_owned()),
+ password: Some(DEFAULT_ROOT_PASSWORD.to_owned()),
+ token: None,
+ consumer: None,
+ }
+ }
+}
+
+impl Default for HttpApiConfig {
+ fn default() -> Self {
+ Self {
+ address: "localhost:8082".to_owned(),
+ path: "/mcp".to_owned(),
+ tls: None,
+ cors: None,
+ }
+ }
+}
+
+impl Default for PermissionsConfig {
+ fn default() -> Self {
+ Self {
+ create: true,
+ read: true,
+ update: true,
+ delete: true,
+ }
+ }
+}
+
+pub fn configure_cors(config: &HttpCorsConfig) -> CorsLayer {
+ let allowed_origins = match &config.allowed_origins {
+ origins if origins.is_empty() => AllowOrigin::default(),
+ origins if origins.first().unwrap() == "*" => AllowOrigin::any(),
+ origins => AllowOrigin::list(origins.iter().map(|s| s.parse().unwrap())),
+ };
+
+ let allowed_headers = config
+ .allowed_headers
+ .iter()
+ .filter(|s| !s.is_empty())
+ .map(|s| s.parse().unwrap())
+ .collect::<Vec<_>>();
+
+ let exposed_headers = config
+ .exposed_headers
+ .iter()
+ .filter(|s| !s.is_empty())
+ .map(|s| s.parse().unwrap())
+ .collect::<Vec<_>>();
+
+ let allowed_methods = config
+ .allowed_methods
+ .iter()
+ .filter(|s| !s.is_empty())
+ .map(|s| match s.to_uppercase().as_str() {
+ "GET" => Method::GET,
+ "POST" => Method::POST,
+ "PUT" => Method::PUT,
+ "DELETE" => Method::DELETE,
+ "HEAD" => Method::HEAD,
+ "OPTIONS" => Method::OPTIONS,
+ "CONNECT" => Method::CONNECT,
+ "PATCH" => Method::PATCH,
+ "TRACE" => Method::TRACE,
+ _ => panic!("Invalid HTTP method: {s}"),
+ })
+ .collect::<Vec<_>>();
+
+ CorsLayer::new()
+ .allow_methods(allowed_methods)
+ .allow_origin(allowed_origins)
+ .allow_headers(allowed_headers)
+ .expose_headers(exposed_headers)
+ .allow_credentials(config.allow_credentials)
+ .allow_private_network(config.allow_private_network)
+}
diff --git a/core/common/src/certificates.rs b/core/ai/mcp/src/error.rs
similarity index 56%
copy from core/common/src/certificates.rs
copy to core/ai/mcp/src/error.rs
index 03056e99..0344cfac 100644
--- a/core/common/src/certificates.rs
+++ b/core/ai/mcp/src/error.rs
@@ -16,16 +16,24 @@
* under the License.
*/
-use rustls::pki_types::{CertificateDer, PrivateKeyDer};
+use thiserror::Error;
-/// Generates a self-signed certificate for the given domain.
-/// Returns a tuple of (certificate chain, private key).
-pub fn generate_self_signed_certificate(
- domain: &str,
-) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Box<dyn std::error::Error>> {
- let cert = rcgen::generate_simple_self_signed(vec![domain.to_string()])?;
- let cert_der = cert.cert.der();
- let key_der = cert.key_pair.serialize_der();
- let key = PrivateKeyDer::try_from(key_der)?;
- Ok((vec![cert_der.clone()], key))
+#[derive(Debug, Error)]
+pub enum McpRuntimeError {
+ #[error("Failed to create service")]
+ FailedToCreateService,
+ #[error("Missing configuration")]
+ MissingConfig,
+ #[error("Failed to start HTTP server")]
+ FailedToStartHttpServer,
+ #[error("Iggy client error")]
+ IggyClient(#[from] iggy::prelude::ClientError),
+ #[error("Iggy error")]
+ IggyError(#[from] iggy::prelude::IggyError),
+ #[error("Missing Iggy credentials")]
+ MissingIggyCredentials,
+ #[error("Failed to create Iggy consumer ID")]
+ FailedToCreateConsumerId,
+ #[error("Invalid API path")]
+ InvalidApiPath,
}
diff --git a/core/ai/mcp/src/main.rs b/core/ai/mcp/src/main.rs
new file mode 100644
index 00000000..1337ce0b
--- /dev/null
+++ b/core/ai/mcp/src/main.rs
@@ -0,0 +1,198 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use config::{Config, Environment, File};
+use configs::{McpServerConfig, McpTransport};
+use dotenvy::dotenv;
+use error::McpRuntimeError;
+use figlet_rs::FIGfont;
+use iggy::prelude::{Client, Identifier};
+use rmcp::{ServiceExt, model::ErrorData, transport::stdio};
+use service::IggyService;
+use std::{env, sync::Arc};
+use tracing::{error, info};
+use tracing_subscriber::{EnvFilter, Registry, layer::SubscriberExt, util::SubscriberInitExt};
+
+mod api;
+mod configs;
+mod error;
+mod service;
+mod stream;
+
+#[tokio::main]
+async fn main() -> Result<(), McpRuntimeError> {
+ let standard_font = FIGfont::standard().unwrap();
+ let figure = standard_font.convert("Iggy MCP Server");
+ eprintln!("{}", figure.unwrap());
+
+ if let Ok(env_path) = std::env::var("IGGY_MCP_ENV_PATH") {
+ if dotenvy::from_path(&env_path).is_ok() {
+ eprintln!("Loaded environment variables from path: {env_path}");
+ }
+ } else if let Ok(path) = dotenv() {
+ eprintln!(
+ "Loaded environment variables from .env file at path: {}",
+ path.display()
+ );
+ }
+
+ let config_path = env::var("IGGY_MCP_CONFIG_PATH").unwrap_or_else(|_| "config".to_string());
+ eprintln!("Configuration file path: {config_path}");
+ let config: McpServerConfig = Config::builder()
+ .add_source(Config::try_from(&McpServerConfig::default()).expect("Failed to init config"))
+ .add_source(File::with_name(&config_path).required(false))
+ .add_source(Environment::with_prefix("IGGY_MCP").separator("_"))
+ .build()
+ .expect("Failed to build runtime config")
+ .try_deserialize()
+ .expect("Failed to deserialize runtime config");
+
+ let transport = config.transport;
+ if transport == McpTransport::Stdio {
+ tracing_subscriber::fmt()
+ .with_env_filter(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("DEBUG")))
+ .with_writer(std::io::stderr)
+ .with_ansi(false)
+ .init();
+ } else {
+ Registry::default()
+ .with(tracing_subscriber::fmt::layer())
+ .with(EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("INFO")))
+ .init();
+ }
+
+ info!("Starting Iggy MCP Server, transport: {transport}...");
+
+ let consumer_id = Identifier::from_str_value(
+ config.iggy.consumer.as_deref().unwrap_or("iggy-mcp"),
+ )
+ .map_err(|error| {
+ error!("Failed to create Iggy consumer ID: {:?}", error);
+ McpRuntimeError::FailedToCreateConsumerId
+ })?;
+ let iggy_consumer = Arc::new(iggy::prelude::Consumer::new(consumer_id));
+ let iggy_client = Arc::new(stream::init(config.iggy).await?);
+ let client_to_shutdown = iggy_client.clone();
+ let permissions = Permissions {
+ create: config.permissions.create,
+ read: config.permissions.read,
+ update: config.permissions.update,
+ delete: config.permissions.delete,
+ };
+
+ if transport == McpTransport::Stdio {
+ let Ok(service) = IggyService::new(iggy_client, iggy_consumer, permissions)
+ .serve(stdio())
+ .await
+ .inspect_err(|e| {
+ error!("Serving error: {:?}", e);
+ })
+ else {
+ error!("Failed to create service");
+ return Err(McpRuntimeError::FailedToCreateService);
+ };
+
+ if let Err(error) = service.waiting().await {
+ error!("waiting error: {:?}", error);
+ }
+ } else {
+ let Some(http_config) = config.http else {
+ error!("HTTP API configuration not found");
+ return Err(McpRuntimeError::MissingConfig);
+ };
+
+ api::init(http_config, iggy_client, iggy_consumer, permissions).await?;
+ }
+
+ #[cfg(unix)]
+ let (mut ctrl_c, mut sigterm) = {
+ use tokio::signal::unix::{SignalKind, signal};
+ (
+ signal(SignalKind::interrupt()).expect("Failed to create SIGINT signal"),
+ signal(SignalKind::terminate()).expect("Failed to create SIGTERM signal"),
+ )
+ };
+
+ #[cfg(unix)]
+ tokio::select! {
+ _ = ctrl_c.recv() => {
+ info!("Received SIGINT. Shutting down Iggy MCP Server...");
+ },
+ _ = sigterm.recv() => {
+ info!("Received SIGTERM. Shutting down Iggy MCP Server...");
+ }
+ }
+
+ client_to_shutdown.shutdown().await?;
+ info!("Iggy MCP Server stopped successfully");
+ Ok(())
+}
+
+#[derive(Debug, Copy, Clone)]
+pub struct Permissions {
+ create: bool,
+ read: bool,
+ update: bool,
+ delete: bool,
+}
+
+impl Permissions {
+ pub fn ensure_read(&self) -> Result<(), ErrorData> {
+ if self.read {
+ Ok(())
+ } else {
+ Err(ErrorData::invalid_request(
+ "Insufficient 'read' permissions",
+ None,
+ ))
+ }
+ }
+
+ pub fn ensure_create(&self) -> Result<(), ErrorData> {
+ if self.create {
+ Ok(())
+ } else {
+ Err(ErrorData::invalid_request(
+ "Insufficient 'create' permissions",
+ None,
+ ))
+ }
+ }
+
+ pub fn ensure_update(&self) -> Result<(), ErrorData> {
+ if self.update {
+ Ok(())
+ } else {
+ Err(ErrorData::invalid_request(
+ "Insufficient 'update' permissions",
+ None,
+ ))
+ }
+ }
+
+ pub fn ensure_delete(&self) -> Result<(), ErrorData> {
+ if self.delete {
+ Ok(())
+ } else {
+ Err(ErrorData::invalid_request(
+ "Insufficient 'delete' permissions",
+ None,
+ ))
+ }
+ }
+}
diff --git a/core/ai/mcp/src/service/mod.rs b/core/ai/mcp/src/service/mod.rs
new file mode 100644
index 00000000..109adee7
--- /dev/null
+++ b/core/ai/mcp/src/service/mod.rs
@@ -0,0 +1,773 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use iggy::prelude::{
+ Consumer, ConsumerGroupClient, ConsumerOffsetClient, Identifier, IggyClient, IggyError,
+ IggyMessage, IggyTimestamp, MessageClient, PartitionClient, Partitioning,
+ PersonalAccessTokenClient, PollingKind, PollingStrategy, SegmentClient, StreamClient,
+ SystemClient, SystemSnapshotType, TopicClient, UserClient, UserStatus,
+};
+use requests::*;
+use rmcp::{
+ ServerHandler,
+ handler::server::{router::tool::ToolRouter, tool::Parameters},
+ model::{CallToolResult, Content, ErrorData, ServerCapabilities, ServerInfo},
+ tool, tool_handler, tool_router,
+};
+use serde::Serialize;
+use std::sync::Arc;
+use tracing::error;
+
+use crate::Permissions;
+mod requests;
+
+#[derive(Debug, Clone)]
+pub struct IggyService {
+ tool_router: ToolRouter<Self>,
+ client: Arc<IggyClient>,
+ consumer: Arc<Consumer>,
+ permissions: Permissions,
+}
+
+#[tool_router]
+impl IggyService {
+ pub fn new(client: Arc<IggyClient>, consumer: Arc<Consumer>, permissions: Permissions) -> Self {
+ Self {
+ tool_router: Self::tool_router(),
+ client,
+ consumer,
+ permissions,
+ }
+ }
+
+ #[tool(description = "Ping")]
+ pub async fn ping(&self) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.ping().await)
+ }
+
+ #[tool(description = "Get stream")]
+ pub async fn get_stream(
+ &self,
+ Parameters(GetStream { stream_id }): Parameters<GetStream>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_stream(&id(&stream_id)?).await)
+ }
+
+ #[tool(description = "Get streams")]
+ pub async fn get_streams(&self) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_streams().await)
+ }
+
+ #[tool(description = "Create stream")]
+ pub async fn create_stream(
+ &self,
+ Parameters(CreateStream { name, stream_id }): Parameters<CreateStream>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_create()?;
+ request(self.client.create_stream(&name, stream_id).await)
+ }
+
+ #[tool(description = "Update stream")]
+ pub async fn update_stream(
+ &self,
+ Parameters(UpdateStream { stream_id, name }): Parameters<UpdateStream>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_update()?;
+ request(self.client.update_stream(&id(&stream_id)?, &name).await)
+ }
+
+ #[tool(description = "Delete stream")]
+ pub async fn delete_stream(
+ &self,
+ Parameters(DeleteStream { stream_id }): Parameters<DeleteStream>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_delete()?;
+ request(self.client.delete_stream(&id(&stream_id)?).await)
+ }
+
+ #[tool(description = "Purge stream")]
+ pub async fn purge_stream(
+ &self,
+ Parameters(PurgeStream { stream_id }): Parameters<PurgeStream>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_delete()?;
+ request(self.client.purge_stream(&id(&stream_id)?).await)
+ }
+
+ #[tool(description = "Get topics")]
+ pub async fn get_topics(
+ &self,
+ Parameters(GetTopics { stream_id }): Parameters<GetTopics>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_topics(&id(&stream_id)?).await)
+ }
+
+ #[tool(description = "Get topic")]
+ pub async fn get_topic(
+ &self,
+ Parameters(GetTopic {
+ stream_id,
+ topic_id,
+ }): Parameters<GetTopic>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(
+ self.client
+ .get_topic(&id(&stream_id)?, &id(&topic_id)?)
+ .await,
+ )
+ }
+
+ #[tool(description = "Create topic")]
+ pub async fn create_topic(
+ &self,
+ Parameters(CreateTopic {
+ stream_id,
+ name,
+ partitions_count,
+ compression_algorithm,
+ replication_factor,
+ topic_id,
+ message_expiry,
+ max_size,
+ }): Parameters<CreateTopic>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_create()?;
+ let compression_algorithm = compression_algorithm
+ .and_then(|ca| ca.parse().ok())
+ .unwrap_or_default();
+ let message_expiry = message_expiry
+ .and_then(|me| me.parse().ok())
+ .unwrap_or_default();
+ let max_size = max_size.and_then(|ms| ms.parse().ok()).unwrap_or_default();
+ request(
+ self.client
+ .create_topic(
+ &id(&stream_id)?,
+ &name,
+ partitions_count,
+ compression_algorithm,
+ replication_factor,
+ topic_id,
+ message_expiry,
+ max_size,
+ )
+ .await,
+ )
+ }
+
+ #[tool(description = "Update topic")]
+ pub async fn update_topic(
+ &self,
+ Parameters(UpdateTopic {
+ stream_id,
+ topic_id,
+ name,
+ compression_algorithm,
+ replication_factor,
+ message_expiry,
+ max_size,
+ }): Parameters<UpdateTopic>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_update()?;
+ let compression_algorithm = compression_algorithm
+ .and_then(|ca| ca.parse().ok())
+ .unwrap_or_default();
+ let message_expiry = message_expiry
+ .and_then(|me| me.parse().ok())
+ .unwrap_or_default();
+ let max_size = max_size.and_then(|ms| ms.parse().ok()).unwrap_or_default();
+ request(
+ self.client
+ .update_topic(
+ &id(&stream_id)?,
+ &id(&topic_id)?,
+ &name,
+ compression_algorithm,
+ replication_factor,
+ message_expiry,
+ max_size,
+ )
+ .await,
+ )
+ }
+
+ #[tool(description = "Delete topic")]
+ pub async fn delete_topic(
+ &self,
+ Parameters(DeleteTopic {
+ stream_id,
+ topic_id,
+ }): Parameters<DeleteTopic>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_delete()?;
+ request(
+ self.client
+ .delete_topic(&id(&stream_id)?, &id(&topic_id)?)
+ .await,
+ )
+ }
+
+ #[tool(description = "Purge topic")]
+ pub async fn purge_topic(
+ &self,
+ Parameters(PurgeTopic {
+ stream_id,
+ topic_id,
+ }): Parameters<PurgeTopic>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_delete()?;
+ request(
+ self.client
+ .purge_topic(&id(&stream_id)?, &id(&topic_id)?)
+ .await,
+ )
+ }
+
+ #[tool(description = "Create partitions")]
+ pub async fn create_partitions(
+ &self,
+ Parameters(CreatePartitions {
+ stream_id,
+ topic_id,
+ partitions_count,
+ }): Parameters<CreatePartitions>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_create()?;
+ request(
+ self.client
+ .create_partitions(&id(&stream_id)?, &id(&topic_id)?, partitions_count)
+ .await,
+ )
+ }
+
+ #[tool(description = "Delete partitions")]
+ pub async fn delete_partitions(
+ &self,
+ Parameters(DeletePartitions {
+ stream_id,
+ topic_id,
+ partitions_count,
+ }): Parameters<DeletePartitions>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_delete()?;
+ request(
+ self.client
+ .delete_partitions(&id(&stream_id)?, &id(&topic_id)?, partitions_count)
+ .await,
+ )
+ }
+
+ #[tool(description = "Delete segments")]
+ pub async fn delete_segments(
+ &self,
+ Parameters(DeleteSegments {
+ stream_id,
+ topic_id,
+ partition_id,
+ segments_count,
+ }): Parameters<DeleteSegments>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_delete()?;
+ request(
+ self.client
+ .delete_segments(
+ &id(&stream_id)?,
+ &id(&topic_id)?,
+ partition_id,
+ segments_count,
+ )
+ .await,
+ )
+ }
+
+ #[tool(description = "Poll messages")]
+ pub async fn poll_messages(
+ &self,
+ Parameters(PollMessages {
+ stream_id,
+ topic_id,
+ partition_id,
+ strategy,
+ offset,
+ timestamp,
+ count,
+ auto_commit,
+ }): Parameters<PollMessages>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ let offset = offset.unwrap_or(0);
+ let count = count.unwrap_or(10);
+ let mut auto_commit = auto_commit.unwrap_or(false);
+ let strategy = if let Some(strategy) = strategy {
+ match strategy.as_str() {
+ "offset" => PollingStrategy::offset(offset),
+ "first" => PollingStrategy::first(),
+ "last" => PollingStrategy::last(),
+ "next" => PollingStrategy::next(),
+ "timestamp" => PollingStrategy::timestamp(IggyTimestamp::from(
+ timestamp.unwrap_or(IggyTimestamp::now().as_micros()),
+ )),
+ _ => PollingStrategy::offset(offset),
+ }
+ } else {
+ PollingStrategy::offset(offset)
+ };
+ if strategy.kind == PollingKind::Next {
+ auto_commit = true;
+ }
+
+ request(
+ self.client
+ .poll_messages(
+ &id(&stream_id)?,
+ &id(&topic_id)?,
+ partition_id,
+ &self.consumer,
+ &strategy,
+ count,
+ auto_commit,
+ )
+ .await,
+ )
+ }
+
+ #[tool(description = "Send messages")]
+ pub async fn send_messages(
+ &self,
+ Parameters(SendMessages {
+ stream_id,
+ topic_id,
+ partition_id,
+ partitioning,
+ messages_key,
+ messages,
+ }): Parameters<SendMessages>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_create()?;
+ let partitioning = if let Some(partitioning) = partitioning {
+ match partitioning.as_str() {
+ "balanced" => Partitioning::balanced(),
+ "key" => {
+ let key = messages_key.unwrap_or_default().as_bytes().to_vec();
+ if key.is_empty() {
+ return Err(ErrorData::invalid_request(
+ "Messages key cannot be empty",
+ None,
+ ));
+ }
+
+ Partitioning::messages_key(&key).map_err(|error| {
+ ErrorData::invalid_request(format!("Invalid messages key: {error}"), None)
+ })?
+ }
+ "partition" => Partitioning::partition_id(partition_id.unwrap_or(1)),
+ _ => Partitioning::balanced(),
+ }
+ } else {
+ Partitioning::balanced()
+ };
+
+ let mut messages = messages
+ .into_iter()
+ .flat_map(|message| {
+ if let Some(id) = message.id {
+ return IggyMessage::builder()
+ .id(id)
+ .payload(message.payload.into())
+ .build();
+ }
+
+ IggyMessage::builder()
+ .payload(message.payload.into())
+ .build()
+ })
+ .collect::<Vec<_>>();
+
+ request(
+ self.client
+ .send_messages(
+ &id(&stream_id)?,
+ &id(&topic_id)?,
+ &partitioning,
+ &mut messages,
+ )
+ .await,
+ )
+ }
+
+ #[tool(description = "Get stats")]
+ pub async fn get_stats(&self) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_stats().await)
+ }
+
+ #[tool(description = "Get me")]
+ pub async fn get_me(&self) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_me().await)
+ }
+
+ #[tool(description = "Get client")]
+ pub async fn get_client(
+ &self,
+ Parameters(GetClient { client_id }): Parameters<GetClient>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_client(client_id).await)
+ }
+
+ #[tool(description = "Get clients")]
+ pub async fn get_clients(&self) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_clients().await)
+ }
+
+ #[tool(description = "Snapshot")]
+ pub async fn snapshot(
+ &self,
+ Parameters(Snapshot { compression, types }): Parameters<Snapshot>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ let compression = compression.and_then(|c| c.parse().ok()).unwrap_or_default();
+ let mut types = types
+ .into_iter()
+ .flatten()
+ .flat_map(|t| t.parse().ok())
+ .collect::<Vec<_>>();
+ if types.is_empty() {
+ types = SystemSnapshotType::all_snapshot_types();
+ }
+
+ request(self.client.snapshot(compression, types).await)
+ }
+
+ #[tool(description = "Get consumer groups")]
+ pub async fn get_consumer_groups(
+ &self,
+ Parameters(GetConsumerGroups {
+ stream_id,
+ topic_id,
+ }): Parameters<GetConsumerGroups>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(
+ self.client
+ .get_consumer_groups(&id(&stream_id)?, &id(&topic_id)?)
+ .await,
+ )
+ }
+
+ #[tool(description = "Get consumer group")]
+ pub async fn get_consumer_group(
+ &self,
+ Parameters(GetConsumerGroup {
+ stream_id,
+ topic_id,
+ group_id,
+ }): Parameters<GetConsumerGroup>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(
+ self.client
+ .get_consumer_group(&id(&stream_id)?, &id(&topic_id)?, &id(&group_id)?)
+ .await,
+ )
+ }
+
+ #[tool(description = "Create consumer group")]
+ pub async fn create_consumer_group(
+ &self,
+ Parameters(CreateConsumerGroup {
+ stream_id,
+ topic_id,
+ name,
+ group_id,
+ }): Parameters<CreateConsumerGroup>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_create()?;
+ request(
+ self.client
+ .create_consumer_group(&id(&stream_id)?, &id(&topic_id)?, &name, group_id)
+ .await,
+ )
+ }
+
+ #[tool(description = "Delete consumer group")]
+ pub async fn delete_consumer_group(
+ &self,
+ Parameters(DeleteConsumerGroup {
+ stream_id,
+ topic_id,
+ group_id,
+ }): Parameters<DeleteConsumerGroup>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_delete()?;
+ request(
+ self.client
+ .delete_consumer_group(&id(&stream_id)?, &id(&topic_id)?, &id(&group_id)?)
+ .await,
+ )
+ }
+
+ #[tool(description = "Get consumer offset")]
+ pub async fn get_consumer_offset(
+ &self,
+ Parameters(GetConsumerOffset {
+ stream_id,
+ topic_id,
+ partition_id,
+ }): Parameters<GetConsumerOffset>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(
+ self.client
+ .get_consumer_offset(
+ &self.consumer,
+ &id(&stream_id)?,
+ &id(&topic_id)?,
+ partition_id,
+ )
+ .await,
+ )
+ }
+
+ #[tool(description = "Store consumer offset")]
+ pub async fn store_consumer_offset(
+ &self,
+ Parameters(StoreConsumerOffset {
+ stream_id,
+ topic_id,
+ partition_id,
+ offset,
+ }): Parameters<StoreConsumerOffset>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(
+ self.client
+ .store_consumer_offset(
+ &self.consumer,
+ &id(&stream_id)?,
+ &id(&topic_id)?,
+ partition_id,
+ offset,
+ )
+ .await,
+ )
+ }
+
+ #[tool(description = "Delete consumer offset")]
+ pub async fn delete_consumer_offset(
+ &self,
+ Parameters(DeleteConsumerOffset {
+ stream_id,
+ topic_id,
+ partition_id,
+ }): Parameters<DeleteConsumerOffset>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(
+ self.client
+ .delete_consumer_offset(
+ &self.consumer,
+ &id(&stream_id)?,
+ &id(&topic_id)?,
+ partition_id,
+ )
+ .await,
+ )
+ }
+
+ #[tool(description = "Get personal access tokens")]
+ pub async fn get_personal_access_tokens(&self) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_personal_access_tokens().await)
+ }
+
+ #[tool(description = "Create personal access token")]
+ pub async fn create_personal_access_token(
+ &self,
+ Parameters(CreatePersonalAccessToken { name, expiry }): Parameters<
+ CreatePersonalAccessToken,
+ >,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ let expiry = expiry
+ .and_then(|expiry| expiry.parse().ok())
+ .unwrap_or_default();
+ request(
+ self.client
+ .create_personal_access_token(&name, expiry)
+ .await,
+ )
+ }
+
+ #[tool(description = "Delete personal access token")]
+ pub async fn delete_personal_access_token(
+ &self,
+ Parameters(DeletePersonalAccessToken { name }): Parameters<DeletePersonalAccessToken>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.delete_personal_access_token(&name).await)
+ }
+
+ #[tool(description = "Get users")]
+ pub async fn get_users(&self) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_users().await)
+ }
+
+ #[tool(description = "Get user")]
+ pub async fn get_user(
+ &self,
+ Parameters(GetUser { user_id }): Parameters<GetUser>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_read()?;
+ request(self.client.get_user(&id(&user_id)?).await)
+ }
+
+ #[tool(description = "Create user")]
+ pub async fn create_user(
+ &self,
+ Parameters(CreateUser {
+ username,
+ password,
+ active,
+ permissions,
+ }): Parameters<CreateUser>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_create()?;
+ let status = if active.unwrap_or_default() {
+ UserStatus::Active
+ } else {
+ UserStatus::Inactive
+ };
+ let permissions = permissions.map(|p| p.into());
+ request(
+ self.client
+ .create_user(&username, &password, status, permissions)
+ .await,
+ )
+ }
+
+ #[tool(description = "Update user")]
+ pub async fn update_user(
+ &self,
+ Parameters(UpdateUser {
+ user_id,
+ username,
+ active,
+ }): Parameters<UpdateUser>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_update()?;
+ let status = active.map(|active| {
+ if active {
+ UserStatus::Active
+ } else {
+ UserStatus::Inactive
+ }
+ });
+ request(
+ self.client
+ .update_user(&id(&user_id)?, username.as_deref(), status)
+ .await,
+ )
+ }
+
+ #[tool(description = "Delete user")]
+ pub async fn delete_user(
+ &self,
+ Parameters(DeleteUser { user_id }): Parameters<DeleteUser>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_delete()?;
+ request(self.client.delete_user(&id(&user_id)?).await)
+ }
+
+ #[tool(description = "Update permissions")]
+ pub async fn update_permissions(
+ &self,
+ Parameters(UpdatePermissions {
+ user_id,
+ permissions,
+ }): Parameters<UpdatePermissions>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_update()?;
+ let permissions = permissions.map(|p| p.into());
+ request(
+ self.client
+ .update_permissions(&id(&user_id)?, permissions)
+ .await,
+ )
+ }
+
+ #[tool(description = "Change password")]
+ pub async fn change_password(
+ &self,
+ Parameters(ChangePassword {
+ user_id,
+ current_password,
+ new_password,
+ }): Parameters<ChangePassword>,
+ ) -> Result<CallToolResult, ErrorData> {
+ self.permissions.ensure_update()?;
+ request(
+ self.client
+ .change_password(&id(&user_id)?, ¤t_password, &new_password)
+ .await,
+ )
+ }
+}
+
+#[tool_handler]
+impl ServerHandler for IggyService {
+ fn get_info(&self) -> ServerInfo {
+ ServerInfo {
+ instructions: Some("Iggy service".into()),
+ capabilities: ServerCapabilities::builder().enable_tools().build(),
+ ..Default::default()
+ }
+ }
+}
+
+fn id(id: &str) -> Result<Identifier, ErrorData> {
+ Identifier::from_str_value(id).map_err(|e| {
+ let message = format!("Failed to parse identifier. {e}");
+ error!(message);
+ ErrorData::invalid_request(message, None)
+ })
+}
+
+fn request(result: Result<impl Sized + Serialize, IggyError>) -> Result<CallToolResult, ErrorData> {
+ let result = result.map_err(|e| {
+ let message = format!("There was an error when invoking the method. {e}");
+ error!(message);
+ ErrorData::invalid_request(message, None)
+ })?;
+
+ let content = Content::json(result).map_err(|error| {
+ let message = format!("Failed to serialize result. {error}");
+ error!(message);
+ ErrorData::internal_error(message, None)
+ })?;
+
+ Ok(CallToolResult::success(vec![content]))
+}
diff --git a/core/ai/mcp/src/service/requests.rs b/core/ai/mcp/src/service/requests.rs
new file mode 100644
index 00000000..1a8aea5c
--- /dev/null
+++ b/core/ai/mcp/src/service/requests.rs
@@ -0,0 +1,559 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use std::collections::HashMap;
+
+use iggy::prelude;
+use rmcp::schemars::{self, JsonSchema};
+use serde::Deserialize;
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GetStream {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct CreateStream {
+ #[schemars(description = "stream name (required, must be unique)")]
+ pub name: String,
+ #[schemars(description = "stream identifier (numeric, optional)")]
+ pub stream_id: Option<u32>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct UpdateStream {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+ #[schemars(description = "stream name (required, must be unique)")]
+ pub name: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct DeleteStream {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct PurgeStream {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GetTopics {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GetTopic {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct CreateTopic {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "name (required, must be unique)")]
+ pub name: String,
+
+ #[schemars(description = "partitions count (required, must be greater than 0)")]
+ pub partitions_count: u32,
+
+ #[schemars(description = "compression algorithm (optional, can be one of 'none', 'gzip')")]
+ pub compression_algorithm: Option<String>,
+
+ #[schemars(description = "replication factor (optional, must be greater than 0)")]
+ pub replication_factor: Option<u8>,
+
+ #[schemars(description = "topic identifier (numeric, optional)")]
+ pub topic_id: Option<u32>,
+
+ #[schemars(description = "message expiry (optional)")]
+ pub message_expiry: Option<String>,
+
+ #[schemars(description = "maximum size (optional)")]
+ pub max_size: Option<String>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct UpdateTopic {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "name (required, must be unique)")]
+ pub name: String,
+
+ #[schemars(description = "compression algorithm (optional, can be one of 'none', 'gzip')")]
+ pub compression_algorithm: Option<String>,
+
+ #[schemars(description = "replication factor (optional, must be greater than 0)")]
+ pub replication_factor: Option<u8>,
+
+ #[schemars(description = "message expiry (optional)")]
+ pub message_expiry: Option<String>,
+
+ #[schemars(description = "maximum size (optional)")]
+ pub max_size: Option<String>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct DeleteTopic {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct PurgeTopic {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct CreatePartitions {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "partitions count (required, must be greater than 0)")]
+ pub partitions_count: u32,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct DeletePartitions {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "partitions count (required, must be greater than 0)")]
+ pub partitions_count: u32,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct DeleteSegments {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "partition identifier (number)")]
+ pub partition_id: u32,
+
+ #[schemars(description = "segments count (required, must be greater than 0)")]
+ pub segments_count: u32,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct PollMessages {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "partition identifier (optional, number)")]
+ pub partition_id: Option<u32>,
+
+ #[schemars(description = "strategy (optional, string)")]
+ pub strategy: Option<String>,
+
+ #[schemars(description = "offset to start from (optional)")]
+ pub offset: Option<u64>,
+
+ #[schemars(description = "timestamp to start from (optional, microseconds from Unix Epoch)")]
+ pub timestamp: Option<u64>,
+
+ #[schemars(description = "count (optional, must be greater than 0)")]
+ pub count: Option<u32>,
+
+ #[schemars(description = "auto commit (optional, boolean)")]
+ pub auto_commit: Option<bool>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct SendMessages {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "partition identifier (optional, number)")]
+ pub partition_id: Option<u32>,
+
+ #[schemars(description = "partitioning (optional, string)")]
+ pub partitioning: Option<String>,
+
+ #[schemars(description = "messages key (optional, string)")]
+ pub messages_key: Option<String>,
+
+ #[schemars(description = "messages collection")]
+ pub messages: Vec<Message>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct Message {
+ #[schemars(description = "message identifier (optional, number)")]
+ pub id: Option<u128>,
+
+ #[schemars(description = "message payload, base64 encoded string")]
+ pub payload: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GetClient {
+ #[schemars(description = "client identifier (number)")]
+ pub client_id: u32,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct Snapshot {
+ #[schemars(description = "compression (optional, string)")]
+ pub compression: Option<String>,
+
+ #[schemars(description = "types")]
+ pub types: Option<Vec<String>>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GetConsumerGroup {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "consumer group identifier (name or number)")]
+ pub group_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GetConsumerGroups {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct CreateConsumerGroup {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "consumer group name (required, must be unique)")]
+ pub name: String,
+
+ #[schemars(description = "consumer group identifier (optional, number)")]
+ pub group_id: Option<u32>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct DeleteConsumerGroup {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "consumer group identifier (name or number)")]
+ pub group_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GetConsumerOffset {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "partition identifier (optional, number)")]
+ pub partition_id: Option<u32>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct StoreConsumerOffset {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "partition identifier (optional, number)")]
+ pub partition_id: Option<u32>,
+
+ #[schemars(description = "offset (required, number)")]
+ pub offset: u64,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct DeleteConsumerOffset {
+ #[schemars(description = "stream identifier (name or number)")]
+ pub stream_id: String,
+
+ #[schemars(description = "topic identifier (name or number)")]
+ pub topic_id: String,
+
+ #[schemars(description = "partition identifier (optional, number)")]
+ pub partition_id: Option<u32>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct CreatePersonalAccessToken {
+ #[schemars(description = "personal access token name")]
+ pub name: String,
+
+ #[schemars(description = "personal access token expiry(optional)")]
+ pub expiry: Option<String>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct DeletePersonalAccessToken {
+ #[schemars(description = "personal access token name")]
+ pub name: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GetUser {
+ #[schemars(description = "user identifier (name or number)")]
+ pub user_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct CreateUser {
+ #[schemars(description = "username")]
+ pub username: String,
+
+ #[schemars(description = "password")]
+ pub password: String,
+
+ #[schemars(description = "active (optional)")]
+ pub active: Option<bool>,
+
+ #[schemars(description = "permissions (optional)")]
+ pub permissions: Option<Permissions>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct UpdateUser {
+ #[schemars(description = "user identifier (name or number)")]
+ pub user_id: String,
+
+ #[schemars(description = "username (optional)")]
+ pub username: Option<String>,
+
+ #[schemars(description = "active (optional)")]
+ pub active: Option<bool>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct UpdatePermissions {
+ #[schemars(description = "user identifier (name or number)")]
+ pub user_id: String,
+
+ #[schemars(description = "permissions (optional)")]
+ pub permissions: Option<Permissions>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct DeleteUser {
+ #[schemars(description = "user identifier (name or number)")]
+ pub user_id: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct ChangePassword {
+ #[schemars(description = "user identifier (name or number)")]
+ pub user_id: String,
+
+ #[schemars(description = "current password")]
+ pub current_password: String,
+
+ #[schemars(description = "new password")]
+ pub new_password: String,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct Permissions {
+ #[schemars(description = "global permissions (optional)")]
+ pub global: Option<GlobalPermissions>,
+
+ #[schemars(description = "stream permissions (optional)")]
+ pub streams: Option<HashMap<u32, StreamPermissions>>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct GlobalPermissions {
+ #[schemars(description = "manage servers (optional)")]
+ pub manage_servers: Option<bool>,
+
+ #[schemars(description = "read servers (optional)")]
+ pub read_servers: Option<bool>,
+
+ #[schemars(description = "manage users (optional)")]
+ pub manage_users: Option<bool>,
+
+ #[schemars(description = "read users (optional)")]
+ pub read_users: Option<bool>,
+
+ #[schemars(description = "manage streams (optional)")]
+ pub manage_streams: Option<bool>,
+
+ #[schemars(description = "read streams (optional)")]
+ pub read_streams: Option<bool>,
+
+ #[schemars(description = "manage topics (optional)")]
+ pub manage_topics: Option<bool>,
+
+ #[schemars(description = "read topics (optional)")]
+ pub read_topics: Option<bool>,
+
+ #[schemars(description = "poll messages (optional)")]
+ pub poll_messages: Option<bool>,
+
+ #[schemars(description = "send messages (optional)")]
+ pub send_messages: Option<bool>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct StreamPermissions {
+ #[schemars(description = "manage stream (optional)")]
+ pub manage_stream: Option<bool>,
+
+ #[schemars(description = "read stream (optional)")]
+ pub read_stream: Option<bool>,
+
+ #[schemars(description = "manage topics (optional)")]
+ pub manage_topics: Option<bool>,
+
+ #[schemars(description = "read topics (optional)")]
+ pub read_topics: Option<bool>,
+
+ #[schemars(description = "poll messages (optional)")]
+ pub poll_messages: Option<bool>,
+
+ #[schemars(description = "send messages (optional)")]
+ pub send_messages: Option<bool>,
+
+ #[schemars(description = "topics permissions (optional)")]
+ pub topics: Option<HashMap<u32, TopicPermissions>>,
+}
+
+#[derive(Debug, Deserialize, JsonSchema)]
+pub struct TopicPermissions {
+ #[schemars(description = "manage topic (optional)")]
+ pub manage_topic: Option<bool>,
+
+ #[schemars(description = "read topic (optional)")]
+ pub read_topic: Option<bool>,
+
+ #[schemars(description = "poll messages (optional)")]
+ pub poll_messages: Option<bool>,
+
+ #[schemars(description = "send messages (optional)")]
+ pub send_messages: Option<bool>,
+}
+
+impl From<Permissions> for prelude::Permissions {
+ fn from(permissions: Permissions) -> Self {
+ prelude::Permissions {
+ global: permissions
+ .global
+ .map(|global| global.into())
+ .unwrap_or_default(),
+ streams: permissions.streams.map(|streams| {
+ streams
+ .into_iter()
+ .map(|(id, stream)| (id, stream.into()))
+ .collect()
+ }),
+ }
+ }
+}
+
+impl From<GlobalPermissions> for prelude::GlobalPermissions {
+ fn from(permissions: GlobalPermissions) -> Self {
+ prelude::GlobalPermissions {
+ manage_servers: permissions.manage_servers.unwrap_or_default(),
+ read_servers: permissions.read_servers.unwrap_or_default(),
+ manage_users: permissions.manage_users.unwrap_or_default(),
+ read_users: permissions.read_users.unwrap_or_default(),
+ manage_streams: permissions.manage_streams.unwrap_or_default(),
+ read_streams: permissions.read_streams.unwrap_or_default(),
+ manage_topics: permissions.manage_topics.unwrap_or_default(),
+ read_topics: permissions.read_topics.unwrap_or_default(),
+ poll_messages: permissions.poll_messages.unwrap_or_default(),
+ send_messages: permissions.send_messages.unwrap_or_default(),
+ }
+ }
+}
+
+impl From<StreamPermissions> for prelude::StreamPermissions {
+ fn from(permissions: StreamPermissions) -> Self {
+ prelude::StreamPermissions {
+ manage_stream: permissions.manage_stream.unwrap_or_default(),
+ read_stream: permissions.read_stream.unwrap_or_default(),
+ manage_topics: permissions.manage_topics.unwrap_or_default(),
+ read_topics: permissions.read_topics.unwrap_or_default(),
+ poll_messages: permissions.poll_messages.unwrap_or_default(),
+ send_messages: permissions.send_messages.unwrap_or_default(),
+ topics: permissions.topics.map(|topics| {
+ topics
+ .into_iter()
+ .map(|(id, topic)| (id, topic.into()))
+ .collect()
+ }),
+ }
+ }
+}
+
+impl From<TopicPermissions> for prelude::TopicPermissions {
+ fn from(permissions: TopicPermissions) -> Self {
+ prelude::TopicPermissions {
+ manage_topic: permissions.manage_topic.unwrap_or_default(),
+ read_topic: permissions.read_topic.unwrap_or_default(),
+ poll_messages: permissions.poll_messages.unwrap_or_default(),
+ send_messages: permissions.send_messages.unwrap_or_default(),
+ }
+ }
+}
diff --git a/core/ai/mcp/src/stream.rs b/core/ai/mcp/src/stream.rs
new file mode 100644
index 00000000..da47faeb
--- /dev/null
+++ b/core/ai/mcp/src/stream.rs
@@ -0,0 +1,62 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+use crate::{configs::IggyConfig, error::McpRuntimeError};
+use iggy::prelude::{Client, IggyClient, IggyClientBuilder};
+use tracing::{error, info};
+
+pub async fn init(config: IggyConfig) -> Result<IggyClient, McpRuntimeError> {
+ let address = config.address;
+ let username = config.username;
+ let password = config.password;
+ let token = config.token;
+
+ let connection_string = if let Some(token) = token {
+ if token.is_empty() {
+ error!("Iggy token cannot be empty (if username and password are not provided)");
+ return Err(McpRuntimeError::MissingIggyCredentials);
+ }
+
+ let redacted_token = token.chars().take(3).collect::<String>();
+ info!("Using token: {redacted_token}*** for Iggy authentication");
+ format!("iggy://{token}@{address}")
+ } else {
+ info!("Using username and password for Iggy authentication");
+ let username = username.ok_or(McpRuntimeError::MissingIggyCredentials)?;
+ if username.is_empty() {
+ error!("Iggy password cannot be empty (if token is not provided)");
+ return Err(McpRuntimeError::MissingIggyCredentials);
+ }
+
+ let password = password.ok_or(McpRuntimeError::MissingIggyCredentials)?;
+ if password.is_empty() {
+ error!("Iggy password cannot be empty (if token is not provided)");
+ return Err(McpRuntimeError::MissingIggyCredentials);
+ }
+
+ let redacted_username = username.chars().take(3).collect::<String>();
+ let redacted_password = password.chars().take(3).collect::<String>();
+ info!(
+ "Using username: {redacted_username}***, password: {redacted_password}*** for Iggy authentication"
+ );
+ format!("iggy://{username}:{password}@{address}")
+ };
+
+ let client = IggyClientBuilder::from_connection_string(&connection_string)?.build()?;
+ client.connect().await?;
+ Ok(client)
+}
diff --git a/core/bench/dashboard/server/Cargo.toml b/core/bench/dashboard/server/Cargo.toml
index d7792b10..42f20670 100644
--- a/core/bench/dashboard/server/Cargo.toml
+++ b/core/bench/dashboard/server/Cargo.toml
@@ -13,8 +13,8 @@ bench-report = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
clap = { workspace = true }
dashmap = { workspace = true }
-file-operation = "0.5.12"
-notify = "8.0.0"
+file-operation = "0.5.16"
+notify = "8.1.0"
octocrab = "0.44.1"
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
diff --git a/core/cli/Cargo.toml b/core/cli/Cargo.toml
index 7b9c3fbc..7491954d 100644
--- a/core/cli/Cargo.toml
+++ b/core/cli/Cargo.toml
@@ -39,7 +39,7 @@ login-session = ["dep:keyring"]
ahash = { workspace = true }
anyhow = { workspace = true }
clap = { workspace = true }
-clap_complete = "4.5.54"
+clap_complete = "4.5.55"
figlet-rs = { workspace = true }
iggy = { workspace = true }
iggy_binary_protocol = { workspace = true }
diff --git a/core/common/Cargo.toml b/core/common/Cargo.toml
index 443b0150..80f92ebc 100644
--- a/core/common/Cargo.toml
+++ b/core/common/Cargo.toml
@@ -46,7 +46,7 @@ crc32fast = { workspace = true }
derive_more = { workspace = true }
fast-async-mutex = { version = "0.6.7", optional = true }
humantime = { workspace = true }
-rcgen = "0.13.2"
+rcgen = "0.14.3"
rustls = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
diff --git a/core/common/src/certificates.rs b/core/common/src/certificates.rs
index 03056e99..f697b776 100644
--- a/core/common/src/certificates.rs
+++ b/core/common/src/certificates.rs
@@ -25,7 +25,7 @@ pub fn generate_self_signed_certificate(
) -> Result<(Vec<CertificateDer<'static>>, PrivateKeyDer<'static>), Box<dyn std::error::Error>> {
let cert = rcgen::generate_simple_self_signed(vec![domain.to_string()])?;
let cert_der = cert.cert.der();
- let key_der = cert.key_pair.serialize_der();
+ let key_der = cert.signing_key.serialize_der();
let key = PrivateKeyDer::try_from(key_der)?;
Ok((vec![cert_der.clone()], key))
}
diff --git a/docker-compose.yml b/core/connectors/runtime/Dockerfile
similarity index 67%
copy from docker-compose.yml
copy to core/connectors/runtime/Dockerfile
index 347bef1b..8ce273db 100644
--- a/docker-compose.yml
+++ b/core/connectors/runtime/Dockerfile
@@ -15,26 +15,17 @@
# specific language governing permissions and limitations
# under the License.
-version: '3.5'
-services:
+FROM rust:latest AS builder
+WORKDIR /build
+COPY . /build
+RUN cargo build --bin iggy-connectors --release
- iggy-server:
- build: .
- container_name: iggy-server
- restart: unless-stopped
- networks:
- - iggy
- ports:
- - 3000:3000
- - 8080:8080
- - 8090:8090
- volumes:
- - iggy-server:/local_data
+FROM debian:bookworm-slim
+RUN apt-get update && apt-get install -y \
+ ca-certificates \
+ liblzma5 \
+ && rm -rf /var/lib/apt/lists/*
+COPY ./core/connectors/runtime/config.toml ./config.toml
+COPY --from=builder /build/target/release/iggy-connectors .
-volumes:
- iggy-server:
- driver: local
-
-networks:
- iggy:
- name: iggy-network
+CMD ["/iggy-connectors"]
diff --git a/core/connectors/runtime/src/api/mod.rs b/core/connectors/runtime/src/api/mod.rs
index b6987d1d..3c875836 100644
--- a/core/connectors/runtime/src/api/mod.rs
+++ b/core/connectors/runtime/src/api/mod.rs
@@ -80,7 +80,7 @@ pub async fn init(config: &HttpApiConfig, context: Arc<RuntimeContext>) {
)
.await
{
- error!("Failed to start {NAME} server, error {}", error);
+ error!("Failed to start {NAME} server, error: {error}");
}
});
return;
@@ -104,7 +104,7 @@ pub async fn init(config: &HttpApiConfig, context: Arc<RuntimeContext>) {
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.await
{
- error!("Failed to start {NAME} server, error: {}", error);
+ error!("Failed to start {NAME} server, error: {error}");
}
});
}
diff --git a/core/connectors/sdk/Cargo.toml b/core/connectors/sdk/Cargo.toml
index fa5804a1..120491a6 100644
--- a/core/connectors/sdk/Cargo.toml
+++ b/core/connectors/sdk/Cargo.toml
@@ -36,14 +36,14 @@ async-trait = { workspace = true }
base64 = "0.22.1"
chrono = { workspace = true }
dashmap = { workspace = true }
-flatbuffers = "24.3.25"
+flatbuffers = "25.2.10"
iggy = { workspace = true }
once_cell = { workspace = true }
postcard = { workspace = true }
-prost = "0.13.5"
-prost-types = "0.13.5"
-protox = "0.8.0"
-protox-parse = "0.8.0"
+prost = "0.14.1"
+prost-types = "0.14.1"
+protox = "0.9.0"
+protox-parse = "0.9.0"
regex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
diff --git a/core/integration/Cargo.toml b/core/integration/Cargo.toml
index b17d3d0b..712aa52f 100644
--- a/core/integration/Cargo.toml
+++ b/core/integration/Cargo.toml
@@ -45,7 +45,14 @@ lazy_static = { workspace = true }
libc = "0.2.174"
log = { workspace = true }
predicates = { workspace = true }
-rcgen = "0.13.2"
+rand = { workspace = true }
+rcgen = "0.14.3"
+reqwest = { workspace = true }
+rmcp = { version = "0.3.0", features = [
+ "client",
+ "reqwest",
+ "transport-streamable-http-client",
+] }
serial_test = { workspace = true }
server = { workspace = true }
tempfile = { workspace = true }
diff --git a/core/integration/src/lib.rs b/core/integration/src/lib.rs
index 1f65508b..88c51ac1 100644
--- a/core/integration/src/lib.rs
+++ b/core/integration/src/lib.rs
@@ -25,5 +25,7 @@ pub mod quic_client;
#[allow(deprecated)]
pub mod tcp_client;
#[allow(deprecated)]
+pub mod test_mcp_server;
+#[allow(deprecated)]
pub mod test_server;
pub mod test_tls_utils;
diff --git a/core/integration/src/test_mcp_server.rs b/core/integration/src/test_mcp_server.rs
new file mode 100644
index 00000000..8334e5f8
--- /dev/null
+++ b/core/integration/src/test_mcp_server.rs
@@ -0,0 +1,232 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use assert_cmd::prelude::CommandCargoExt;
+use rand::Rng;
+use rmcp::{
+ RoleClient, ServiceExt,
+ model::{ClientCapabilities, ClientInfo, Implementation, InitializeRequestParam},
+ service::RunningService,
+ transport::StreamableHttpClientTransport,
+};
+use std::fs::OpenOptions;
+use std::io::Write;
+use std::net::{Ipv4Addr, SocketAddr};
+use std::path::PathBuf;
+use std::process::{Child, Command};
+use std::time::Duration;
+use std::{collections::HashMap, net::TcpListener};
+use tokio::time::sleep;
+
+pub const CONSUMER_NAME: &str = "mcp";
+const MCP_PATH: &str = "/mcp";
+
+pub type McpClient = RunningService<RoleClient, InitializeRequestParam>;
+
+#[derive(Debug)]
+pub struct TestMcpServer {
+ envs: HashMap<String, String>,
+ child_handle: Option<Child>,
+ server_address: SocketAddr,
+ stdout_file_path: Option<PathBuf>,
+ stderr_file_path: Option<PathBuf>,
+ server_executable_path: Option<String>,
+}
+
+impl TestMcpServer {
+ pub fn with_iggy_address(iggy_tcp_server_address: &str) -> Self {
+ Self::new(iggy_tcp_server_address, None, None)
+ }
+
+ pub fn new(
+ iggy_tcp_server_address: &str,
+ extra_envs: Option<HashMap<String, String>>,
+ server_executable_path: Option<String>,
+ ) -> Self {
+ let mut envs = HashMap::new();
+ if let Some(extra) = extra_envs {
+ for (key, value) in extra {
+ envs.insert(key, value);
+ }
+ }
+
+ envs.insert("IGGY_MCP_HTTP_PATH".to_string(), MCP_PATH.to_string());
+ envs.insert(
+ "IGGY_MCP_IGGY_ADDRESS".to_string(),
+ iggy_tcp_server_address.to_string(),
+ );
+ envs.insert(
+ "IGGY_MCP_IGGY_CONSUMER".to_string(),
+ CONSUMER_NAME.to_string(),
+ );
+ envs.insert("IGGY_MCP_TRANSPORT".to_string(), "http".to_string());
+ Self::create(envs, server_executable_path)
+ }
+
+ pub fn create(envs: HashMap<String, String>, server_executable_path: Option<String>) -> Self {
+ let server_address = Self::get_random_server_address();
+
+ Self {
+ envs,
+ child_handle: None,
+ server_address,
+ stdout_file_path: None,
+ stderr_file_path: None,
+ server_executable_path,
+ }
+ }
+
+ pub fn start(&mut self) {
+ self.envs
+ .entry("IGGY_MCP_HTTP_ADDRESS".to_string())
+ .or_insert(self.server_address.to_string());
+ let mut command = if let Some(server_executable_path) = &self.server_executable_path {
+ Command::new(server_executable_path)
+ } else {
+ Command::cargo_bin("iggy-mcp").unwrap()
+ };
+ command.envs(self.envs.clone());
+ let child = command.spawn().unwrap();
+ self.child_handle = Some(child);
+ }
+
+ pub fn stop(&mut self) {
+ #[allow(unused_mut)]
+ if let Some(mut child_handle) = self.child_handle.take() {
+ #[cfg(unix)]
+ unsafe {
+ use libc::SIGTERM;
+ use libc::kill;
+ kill(child_handle.id() as libc::pid_t, SIGTERM);
+ }
+
+ #[cfg(not(unix))]
+ child_handle.kill().unwrap();
+
+ if let Ok(output) = child_handle.wait_with_output() {
+ let stderr = String::from_utf8_lossy(&output.stderr);
+ let stdout = String::from_utf8_lossy(&output.stdout);
+ if let Some(stderr_file_path) = &self.stderr_file_path {
+ OpenOptions::new()
+ .append(true)
+ .create(true)
+ .open(stderr_file_path)
+ .unwrap()
+ .write_all(stderr.as_bytes())
+ .unwrap();
+ }
+
+ if let Some(stdout_file_path) = &self.stdout_file_path {
+ OpenOptions::new()
+ .append(true)
+ .create(true)
+ .open(stdout_file_path)
+ .unwrap()
+ .write_all(stdout.as_bytes())
+ .unwrap();
+ }
+ }
+ }
+ }
+
+ pub fn is_started(&self) -> bool {
+ self.child_handle.is_some()
+ }
+
+ pub fn pid(&self) -> u32 {
+ self.child_handle.as_ref().unwrap().id()
+ }
+
+ fn get_http_mcp_api_address(&self) -> String {
+ format!("{}{MCP_PATH}", self.get_http_api_address())
+ }
+
+ fn get_http_api_address(&self) -> String {
+ format!(
+ "http://{}:{}",
+ self.server_address.ip(),
+ self.server_address.port()
+ )
+ }
+
+ pub async fn ensure_started(&self) {
+ let http_api_address = self.get_http_api_address();
+ let client = reqwest::Client::new();
+ let max_retries = 3000;
+ let mut retries = 0;
+ while let Err(error) = client.get(&http_api_address).send().await {
+ sleep(Duration::from_millis(20)).await;
+ retries += 1;
+ if retries >= max_retries {
+ panic!(
+ "Failed to ping MCP server: {http_api_address} after {max_retries} retries. {error}"
+ );
+ }
+ }
+ println!("MCP server address started at: {http_api_address}");
+ }
+
+ pub async fn get_client(&self) -> McpClient {
+ let mcp_http_api_address = self.get_http_mcp_api_address();
+ let transport = StreamableHttpClientTransport::from_uri(mcp_http_api_address);
+ let client_info = ClientInfo {
+ protocol_version: Default::default(),
+ capabilities: ClientCapabilities::default(),
+ client_info: Implementation {
+ name: "test-mcp-client".to_string(),
+ version: "1.0.0".to_string(),
+ },
+ };
+ client_info
+ .serve(transport)
+ .await
+ .inspect_err(|error| {
+ eprintln!("MCP client error: {error}");
+ })
+ .expect("Failed to create MCP client")
+ }
+
+ fn get_random_server_address() -> SocketAddr {
+ let mut rng = rand::thread_rng();
+ let max_retries = 100;
+
+ for _ in 0..max_retries {
+ #[cfg(target_os = "linux")]
+ let port = rng.gen_range(20000..=29999);
+
+ #[cfg(target_os = "macos")]
+ let port = rng.gen_range(20000..=49151);
+
+ #[cfg(target_os = "windows")]
+ let port = rng.gen_range(20000..=49151);
+
+ let addr = SocketAddr::new(Ipv4Addr::LOCALHOST.into(), port);
+ if TcpListener::bind(addr).is_ok() {
+ return addr;
+ }
+ }
+
+ panic!("Failed to find a free port after {max_retries} retries");
+ }
+}
+
+impl Drop for TestMcpServer {
+ fn drop(&mut self) {
+ self.stop();
+ }
+}
diff --git a/core/integration/src/test_tls_utils.rs b/core/integration/src/test_tls_utils.rs
index 53dd44c6..75b75abc 100644
--- a/core/integration/src/test_tls_utils.rs
+++ b/core/integration/src/test_tls_utils.rs
@@ -38,7 +38,7 @@ pub fn generate_test_certificates(cert_dir: &str) -> Result<(), Box<dyn std::err
let key_path = Path::new(cert_dir).join("test_key.pem");
let mut key_file = File::create(&key_path)?;
- key_file.write_all(cert.key_pair.serialize_pem().as_bytes())?;
+ key_file.write_all(cert.signing_key.serialize_pem().as_bytes())?;
Ok(())
}
diff --git a/core/integration/tests/mcp/mod.rs b/core/integration/tests/mcp/mod.rs
new file mode 100644
index 00000000..88134d72
--- /dev/null
+++ b/core/integration/tests/mcp/mod.rs
@@ -0,0 +1,701 @@
+/* Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+use iggy::prelude::{Client, DEFAULT_ROOT_PASSWORD, DEFAULT_ROOT_USERNAME, IggyClient};
+use iggy_binary_protocol::{
+ ConsumerGroupClient, ConsumerOffsetClient, MessageClient, PersonalAccessTokenClient,
+ StreamClient, TopicClient, UserClient,
+};
+use iggy_common::{
+ ClientInfo, ClientInfoDetails, Consumer, ConsumerGroup, ConsumerGroupDetails,
+ ConsumerOffsetInfo, Identifier, IggyExpiry, IggyMessage, MaxTopicSize, Partitioning,
+ PersonalAccessTokenExpiry, PersonalAccessTokenInfo, PolledMessages, RawPersonalAccessToken,
+ Snapshot, Stats, Stream, StreamDetails, Topic, TopicDetails, UserInfo, UserInfoDetails,
+ UserStatus,
+};
+use integration::{
+ test_mcp_server::{CONSUMER_NAME, McpClient, TestMcpServer},
+ test_server::{IpAddrKind, TestServer},
+};
+use lazy_static::lazy_static;
+use rmcp::{
+ ServiceError,
+ model::{CallToolRequestParam, CallToolResult, ListToolsResult},
+ serde::de::DeserializeOwned,
+ serde_json::{self, json},
+};
+use serial_test::parallel;
+use std::collections::HashMap;
+
+const STREAM_NAME: &str = "test_stream";
+const TOPIC_NAME: &str = "test_topic";
+const MESSAGE_PAYLOAD: &str = "test_message";
+const CONSUMER_GROUP_NAME: &str = "test_consumer_group";
+const PERSONAL_ACCESS_TOKEN_NAME: &str = "test_personal_access_token";
+const USER_NAME: &str = "test_user";
+const USER_PASSWORD: &str = "secret";
+
+lazy_static! {
+ static ref STREAM_ID: Identifier =
+ Identifier::from_str_value(STREAM_NAME).expect("Failed to create stream ID");
+ static ref TOPIC_ID: Identifier =
+ Identifier::from_str_value(TOPIC_NAME).expect("Failed to create topic ID");
+ static ref CONSUMER_GROUP_ID: Identifier =
+ Identifier::from_str_value(CONSUMER_GROUP_NAME).expect("Failed to create group ID");
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_list_tools() {
+ let infra = setup().await;
+ let client = infra.mcp_client;
+ let tools = client.list_tools().await.expect("Failed to list tools");
+
+ assert!(!tools.tools.is_empty());
+ let tools_count = tools.tools.len();
+ assert_eq!(tools_count, 40);
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_handle_ping() {
+ assert_empty_response("ping", None).await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_list_of_streams() {
+ assert_response::<Vec<Stream>>("get_streams", None, |streams| {
+ assert_eq!(streams.len(), 1);
+ let stream = &streams[0];
+ assert_eq!(&stream.name, STREAM_NAME);
+ assert_eq!(&stream.topics_count, &1);
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_stream_details() {
+ assert_response::<StreamDetails>(
+ "get_stream",
+ Some(json!({"stream_id": STREAM_NAME})),
+ |stream| {
+ assert_eq!(stream.name, STREAM_NAME);
+ assert_eq!(stream.topics_count, 1);
+ assert_eq!(stream.messages_count, 1);
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_create_stream() {
+ let name = "new_stream";
+ assert_response::<StreamDetails>("create_stream", Some(json!({ "name": name})), |stream| {
+ assert_eq!(stream.name, name);
+ assert_eq!(stream.topics_count, 0);
+ assert_eq!(stream.messages_count, 0);
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_update_stream() {
+ let name = "updated_stream";
+ assert_empty_response(
+ "update_stream",
+ Some(json!({"stream_id": STREAM_NAME, "name": name})),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_delete_stream() {
+ assert_empty_response("delete_stream", Some(json!({"stream_id": STREAM_NAME}))).await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_purge_stream() {
+ assert_empty_response("purge_stream", Some(json!({"stream_id": STREAM_NAME}))).await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_list_of_topics() {
+ assert_response::<Vec<Topic>>(
+ "get_topics",
+ Some(json!({"stream_id": STREAM_NAME})),
+ |topics| {
+ assert_eq!(topics.len(), 1);
+ let topic = &topics[0];
+ assert_eq!(topic.name, TOPIC_NAME);
+ assert_eq!(topic.partitions_count, 1);
+ assert_eq!(topic.messages_count, 1);
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_topic_details() {
+ assert_response::<TopicDetails>(
+ "get_topic",
+ Some(json!({"stream_id": STREAM_NAME, "topic_id": TOPIC_NAME})),
+ |topic| {
+ assert_eq!(topic.id, 1);
+ assert_eq!(topic.name, TOPIC_NAME);
+ assert_eq!(topic.messages_count, 1);
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_create_topic() {
+ let name = "new_topic";
+ assert_response::<TopicDetails>(
+ "create_topic",
+ Some(json!({ "stream_id": STREAM_NAME, "name": name, "partitions_count": 1})),
+ |topic| {
+ assert_eq!(topic.id, 2);
+ assert_eq!(topic.name, name);
+ assert_eq!(topic.partitions_count, 1);
+ assert_eq!(topic.messages_count, 0);
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_update_topic() {
+ let name = "updated_topic";
+ assert_empty_response(
+ "update_topic",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "name": name})),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_delete_topic() {
+ assert_empty_response(
+ "delete_topic",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_purge_topic() {
+ assert_empty_response(
+ "purge_topic",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_create_partitions() {
+ assert_empty_response(
+ "create_partitions",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "partitions_count": 3 })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_delete_partitions() {
+ assert_empty_response(
+ "delete_partitions",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "partitions_count": 1 })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_delete_segments() {
+ assert_empty_response(
+ "delete_segments",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "partition_id": 1, "segments_count": 1 })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_poll_messages() {
+ assert_response::<PolledMessages>(
+ "poll_messages",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "partition_id": 1, "offset": 0 })),
+ |messages| {
+ assert_eq!(messages.messages.len(), 1);
+ let message = &messages.messages[0];
+ assert_eq!(message.header.offset, 0);
+ let payload = message.payload_as_string().expect("Failed to parse message payload");
+ assert_eq!(payload, MESSAGE_PAYLOAD);
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_send_messages() {
+ assert_empty_response(
+ "send_messages",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "partition_id": 1, "messages": [
+ {
+ "payload": "test"
+ }
+ ] })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_stats() {
+ assert_response::<Stats>("get_stats", None, |stats| {
+ assert!(!stats.hostname.is_empty());
+ assert_eq!(stats.messages_count, 1);
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_me() {
+ assert_response::<ClientInfoDetails>("get_me", None, |client| {
+ assert!(client.client_id > 0);
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_clients() {
+ assert_response::<Vec<ClientInfo>>("get_clients", None, |clients| {
+ assert!(!clients.is_empty());
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_handle_snapshot() {
+ assert_response::<Snapshot>("snapshot", None, |snapshot| {
+ assert!(!snapshot.0.is_empty());
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_consumer_groups() {
+ assert_response::<Vec<ConsumerGroup>>(
+ "get_consumer_groups",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME})),
+ |groups| {
+ assert!(!groups.is_empty());
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_consumer_group_details() {
+ assert_response::<ConsumerGroupDetails>("get_consumer_group", Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "group_id": CONSUMER_GROUP_NAME })), |group| {
+ assert_eq!(group.name, CONSUMER_GROUP_NAME);
+ assert_eq!(group.partitions_count, 1);
+ assert_eq!(group.members_count, 0);
+ assert!(group.members.is_empty());
+
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_create_consumer_group() {
+ let name = "test";
+ assert_response::<ConsumerGroupDetails>(
+ "create_consumer_group",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "name": name })),
+ |group| {
+ assert_eq!(group.name, name);
+ assert_eq!(group.partitions_count, 1);
+ assert_eq!(group.members_count, 0);
+ assert!(group.members.is_empty());
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_delete_consumer_group() {
+ assert_empty_response(
+ "delete_consumer_group",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "group_id": CONSUMER_GROUP_NAME })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_consumer_offset() {
+ assert_response::<Option<ConsumerOffsetInfo>>(
+ "get_consumer_offset",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "partition_id": 1 })),
+ |offset| {
+ assert!(offset.is_some());
+ let offset = offset.unwrap();
+ assert_eq!(offset.partition_id, 1);
+ assert_eq!(offset.stored_offset, 0);
+ assert_eq!(offset.current_offset, 0);
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_store_consumer_offset() {
+ assert_empty_response(
+ "store_consumer_offset",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "partition_id": 1, "offset": 0 })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_delete_consumer_offset() {
+ assert_empty_response(
+ "delete_consumer_offset",
+ Some(json!({ "stream_id": STREAM_NAME, "topic_id": TOPIC_NAME, "partition_id": 1, "offset": 0 })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_personal_access_tokens() {
+ assert_response::<Vec<PersonalAccessTokenInfo>>("get_personal_access_tokens", None, |tokens| {
+ assert_eq!(tokens.len(), 1);
+ assert_eq!(tokens[0].name, PERSONAL_ACCESS_TOKEN_NAME);
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_create_personal_access_token() {
+ let name = "test_token";
+ let expiry = PersonalAccessTokenExpiry::NeverExpire.to_string();
+ assert_response::<RawPersonalAccessToken>(
+ "create_personal_access_token",
+ Some(json!({ "name": name, "expiry": expiry })),
+ |token| {
+ assert!(!token.token.is_empty());
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_delete_personal_access_token() {
+ assert_empty_response(
+ "delete_personal_access_token",
+ Some(json!({ "name": PERSONAL_ACCESS_TOKEN_NAME})),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_users() {
+ assert_response::<Vec<UserInfo>>("get_users", None, |users| {
+ assert_eq!(users.len(), 2);
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_return_user_details() {
+ assert_response::<UserInfoDetails>("get_user", Some(json!({ "user_id": USER_NAME})), |user| {
+ assert_eq!(user.username, USER_NAME);
+ })
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_create_user() {
+ let username = "test-mcp-user";
+ assert_response::<UserInfoDetails>(
+ "create_user",
+ Some(json!({ "username": username, "password": "secret"})),
+ |user| {
+ assert_eq!(user.username, username);
+ },
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_update_user() {
+ assert_empty_response(
+ "update_user",
+ Some(json!({ "user_id": USER_NAME, "username": "test-mcp-user", "active": false})),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_delete_user() {
+ assert_empty_response("delete_user", Some(json!({ "user_id": USER_NAME}))).await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_update_permissions() {
+ let permissions = json!({
+ "global": {
+ "manage_servers": true,
+ "read_users": true,
+ },
+ "streams": {
+ "1": {
+ "manage_stream": true,
+ "manage_topics": true,
+ "topics": {
+ "1": {
+ "manage_topic": true,
+ "read_topic": true,
+ "poll_messages": true,
+ "send_messages": true,
+ }
+ }
+ }
+ }
+ });
+
+ assert_empty_response(
+ "update_permissions",
+ Some(json!({ "user_id": USER_NAME, "permissions": permissions })),
+ )
+ .await;
+}
+
+#[tokio::test]
+#[parallel]
+async fn mcp_server_should_change_password() {
+ assert_empty_response(
+ "change_password",
+ Some(json!({ "user_id": USER_NAME, "current_password": USER_PASSWORD, "new_password": "secret2"})),
+ )
+ .await;
+}
+
+async fn assert_empty_response(method: &str, data: Option<serde_json::Value>) {
+ assert_response::<()>(method, data, |()| {}).await
+}
+
+async fn assert_response<T: DeserializeOwned>(
+ method: &str,
+ data: Option<serde_json::Value>,
+ assert_response: impl FnOnce(T),
+) {
+ let infra = setup().await;
+ let client = infra.mcp_client;
+ let result = invoke_request(&client, method, data).await;
+ assert_response(result)
+}
+
+async fn invoke_request<T: DeserializeOwned>(
+ client: &TestMcpClient,
+ method: &str,
+ data: Option<serde_json::Value>,
+) -> T {
+ let error_message = format!("Failed to invoke MCP method: {method}",);
+ let mut result = client.invoke(method, data).await.expect(&error_message);
+
+ if result.content.is_empty() {
+ panic!("No content returned from MCP for method: {method}");
+ }
+
+ let result = result.content.remove(0);
+ let Some(text) = result.as_text() else {
+ panic!("Expected text response for MCP method: {method}");
+ };
+
+ serde_json::from_str::<T>(&text.text).expect("Failed to parse JSON")
+}
+
+async fn setup() -> McpInfra {
+ let mut iggy_envs = HashMap::new();
+ iggy_envs.insert("IGGY_QUIC_ENABLED".to_owned(), "false".to_owned());
+ let mut test_server = TestServer::new(Some(iggy_envs), true, None, IpAddrKind::V4);
+ test_server.start();
+ let iggy_server_address = test_server
+ .get_raw_tcp_addr()
+ .expect("Failed to get Iggy TCP address");
+ seed_data(&iggy_server_address).await;
+
+ let mut test_mcp_server = TestMcpServer::with_iggy_address(&iggy_server_address);
+ test_mcp_server.start();
+ test_mcp_server.ensure_started().await;
+ let mcp_client = test_mcp_server.get_client().await;
+
+ McpInfra {
+ _iggy_server: test_server,
+ _mcp_server: test_mcp_server,
+ mcp_client: TestMcpClient { mcp_client },
+ }
+}
+
+async fn seed_data(iggy_server_address: &str) {
+ let iggy_port = iggy_server_address
+ .split(':')
+ .next_back()
+ .unwrap()
+ .parse::<u16>()
+ .unwrap();
+
+ let iggy_client = IggyClient::from_connection_string(&format!(
+ "iggy://{DEFAULT_ROOT_USERNAME}:{DEFAULT_ROOT_PASSWORD}@localhost:{iggy_port}"
+ ))
+ .expect("Failed to create Iggy client");
+
+ iggy_client
+ .connect()
+ .await
+ .expect("Failed to initialize Iggy client");
+
+ iggy_client
+ .create_stream(STREAM_NAME, None)
+ .await
+ .expect("Failed to create stream");
+
+ iggy_client
+ .create_topic(
+ &STREAM_ID,
+ TOPIC_NAME,
+ 1,
+ iggy_common::CompressionAlgorithm::None,
+ None,
+ None,
+ IggyExpiry::ServerDefault,
+ MaxTopicSize::ServerDefault,
+ )
+ .await
+ .expect("Failed to create topic");
+
+ let mut messages = vec![
+ IggyMessage::builder()
+ .payload(MESSAGE_PAYLOAD.into())
+ .build()
+ .expect("Failed to build message"),
+ ];
+
+ iggy_client
+ .send_messages(
+ &STREAM_ID,
+ &TOPIC_ID,
+ &Partitioning::partition_id(1),
+ &mut messages,
+ )
+ .await
+ .expect("Failed to send messages");
+
+ let consumer =
+ Consumer::new(Identifier::named(CONSUMER_NAME).expect("Failed to create consumer"));
+
+ iggy_client
+ .store_consumer_offset(&consumer, &STREAM_ID, &TOPIC_ID, Some(1), 0)
+ .await
+ .expect("Failed to store consumer offset");
+
+ iggy_client
+ .create_consumer_group(&STREAM_ID, &TOPIC_ID, CONSUMER_GROUP_NAME, None)
+ .await
+ .expect("Failed to create consumer group");
+
+ iggy_client
+ .create_user(USER_NAME, USER_PASSWORD, UserStatus::Active, None)
+ .await
+ .expect("Failed to create user");
+
+ iggy_client
+ .create_personal_access_token(
+ PERSONAL_ACCESS_TOKEN_NAME,
+ PersonalAccessTokenExpiry::NeverExpire,
+ )
+ .await
+ .expect("Failed to create personal access token");
+}
+
+#[derive(Debug)]
+struct McpInfra {
+ _iggy_server: TestServer,
+ _mcp_server: TestMcpServer,
+ mcp_client: TestMcpClient,
+}
+
+#[derive(Debug)]
+struct TestMcpClient {
+ mcp_client: McpClient,
+}
+
+impl TestMcpClient {
+ pub async fn list_tools(&self) -> Result<ListToolsResult, ServiceError> {
+ self.mcp_client.list_tools(Default::default()).await
+ }
+
+ pub async fn invoke(
+ &self,
+ method: &str,
+ data: Option<serde_json::Value>,
+ ) -> Result<CallToolResult, ServiceError> {
+ self.mcp_client
+ .call_tool(CallToolRequestParam {
+ name: method.to_owned().into(),
+ arguments: data.and_then(|value| value.as_object().cloned()),
+ })
+ .await
+ }
+}
diff --git a/core/integration/tests/mod.rs b/core/integration/tests/mod.rs
index df7e3a1a..7dbf56bd 100644
--- a/core/integration/tests/mod.rs
+++ b/core/integration/tests/mod.rs
@@ -29,6 +29,7 @@ mod archiver;
mod cli;
mod config_provider;
mod data_integrity;
+mod mcp;
mod sdk;
mod server;
mod state;
diff --git a/docker-compose.yml b/docker-compose.yml
index 347bef1b..48da4ee0 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -15,9 +15,7 @@
# specific language governing permissions and limitations
# under the License.
-version: '3.5'
services:
-
iggy-server:
build: .
container_name: iggy-server