diff --git a/runtime/binding-mcp/COPYRIGHT b/runtime/binding-mcp/COPYRIGHT new file mode 100644 index 0000000000..8b1b7215ef --- /dev/null +++ b/runtime/binding-mcp/COPYRIGHT @@ -0,0 +1,13 @@ +Copyright ${copyrightYears} Aklivity Inc. + +Aklivity 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. diff --git a/runtime/binding-mcp/LICENSE b/runtime/binding-mcp/LICENSE new file mode 100644 index 0000000000..f3cf11c3ad --- /dev/null +++ b/runtime/binding-mcp/LICENSE @@ -0,0 +1,114 @@ + Aklivity Community License Agreement + Version 1.0 + +This Aklivity Community License Agreement Version 1.0 (the “Agreement”) sets +forth the terms on which Aklivity, Inc. (“Aklivity”) makes available certain +software made available by Aklivity under this Agreement (the “Software”). BY +INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF THE SOFTWARE, +YOU AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE TO +SUCH TERMS AND CONDITIONS, YOU MUST NOT USE THE SOFTWARE. IF YOU ARE RECEIVING +THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU +HAVE THE ACTUAL AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS +AGREEMENT ON BEHALF OF SUCH ENTITY. “Licensee” means you, an individual, or +the entity on whose behalf you are receiving the Software. + + 1. LICENSE GRANT AND CONDITIONS. + + 1.1 License. Subject to the terms and conditions of this Agreement, + Aklivity hereby grants to Licensee a non-exclusive, royalty-free, + worldwide, non-transferable, non-sublicenseable license during the term + of this Agreement to: (a) use the Software; (b) prepare modifications and + derivative works of the Software; (c) distribute the Software (including + without limitation in source code or object code form); and (d) reproduce + copies of the Software (the “License”). Licensee is not granted the + right to, and Licensee shall not, exercise the License for an Excluded + Purpose. For purposes of this Agreement, “Excluded Purpose” means making + available any software-as-a-service, platform-as-a-service, + infrastructure-as-a-service or other similar online service that competes + with Aklivity products or services that provide the Software. + + 1.2 Conditions. In consideration of the License, Licensee’s distribution + of the Software is subject to the following conditions: + + (a) Licensee must cause any Software modified by Licensee to carry + prominent notices stating that Licensee modified the Software. + + (b) On each Software copy, Licensee shall reproduce and not remove or + alter all Aklivity or third party copyright or other proprietary + notices contained in the Software, and Licensee must provide the + notice below with each copy. + + “This software is made available by Aklivity, Inc., under the + terms of the Aklivity Community License Agreement, Version 1.0 + located at http://www.Aklivity.io/Aklivity-community-license. BY + INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF + THE SOFTWARE, YOU AGREE TO THE TERMS OF SUCH LICENSE AGREEMENT.” + + 1.3 Licensee Modifications. Licensee may add its own copyright notices + to modifications made by Licensee and may provide additional or different + license terms and conditions for use, reproduction, or distribution of + Licensee’s modifications. While redistributing the Software or + modifications thereof, Licensee may choose to offer, for a fee or free of + charge, support, warranty, indemnity, or other obligations. Licensee, and + not Aklivity, will be responsible for any such obligations. + + 1.4 No Sublicensing. The License does not include the right to + sublicense the Software, however, each recipient to which Licensee + provides the Software may exercise the Licenses so long as such recipient + agrees to the terms and conditions of this Agreement. + + 2. TERM AND TERMINATION. This Agreement will continue unless and until + earlier terminated as set forth herein. If Licensee breaches any of its + conditions or obligations under this Agreement, this Agreement will + terminate automatically and the License will terminate automatically and + permanently. + + 3. INTELLECTUAL PROPERTY. As between the parties, Aklivity will retain all + right, title, and interest in the Software, and all intellectual property + rights therein. Aklivity hereby reserves all rights not expressly granted + to Licensee in this Agreement. Aklivity hereby reserves all rights in its + trademarks and service marks, and no licenses therein are granted in this + Agreement. + + 4. DISCLAIMER. Aklivity HEREBY DISCLAIMS ANY AND ALL WARRANTIES AND + CONDITIONS, EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, AND SPECIFICALLY + DISCLAIMS ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR + PURPOSE, WITH RESPECT TO THE SOFTWARE. + + 5. LIMITATION OF LIABILITY. Aklivity WILL NOT BE LIABLE FOR ANY DAMAGES OF + ANY KIND, INCLUDING BUT NOT LIMITED TO, LOST PROFITS OR ANY CONSEQUENTIAL, + SPECIAL, INCIDENTAL, INDIRECT, OR DIRECT DAMAGES, HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, ARISING OUT OF THIS AGREEMENT. THE FOREGOING SHALL + APPLY TO THE EXTENT PERMITTED BY APPLICABLE LAW. + + 6.GENERAL. + + 6.1 Governing Law. This Agreement will be governed by and interpreted in + accordance with the laws of the state of California, without reference to + its conflict of laws principles. If Licensee is located within the + United States, all disputes arising out of this Agreement are subject to + the exclusive jurisdiction of courts located in Santa Clara County, + California. USA. If Licensee is located outside of the United States, + any dispute, controversy or claim arising out of or relating to this + Agreement will be referred to and finally determined by arbitration in + accordance with the JAMS International Arbitration Rules. The tribunal + will consist of one arbitrator. The place of arbitration will be Palo + Alto, California. The language to be used in the arbitral proceedings + will be English. Judgment upon the award rendered by the arbitrator may + be entered in any court having jurisdiction thereof. + + 6.2 Assignment. Licensee is not authorized to assign its rights under + this Agreement to any third party. Aklivity may freely assign its rights + under this Agreement to any third party. + + 6.3 Other. This Agreement is the entire agreement between the parties + regarding the subject matter hereof. No amendment or modification of + this Agreement will be valid or binding upon the parties unless made in + writing and signed by the duly authorized representatives of both + parties. In the event that any provision, including without limitation + any condition, of this Agreement is held to be unenforceable, this + Agreement and all licenses and rights granted hereunder will immediately + terminate. Waiver by Aklivity of a breach of any provision of this + Agreement or the failure by Aklivity to exercise any right hereunder + will not be construed as a waiver of any subsequent breach of that right + or as a waiver of any other right. \ No newline at end of file diff --git a/runtime/binding-mcp/NOTICE b/runtime/binding-mcp/NOTICE new file mode 100644 index 0000000000..9024d8926d --- /dev/null +++ b/runtime/binding-mcp/NOTICE @@ -0,0 +1,13 @@ +Licensed under the Aklivity Community License (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + https://www.aklivity.io/aklivity-community-license/ + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +This project includes: + diff --git a/runtime/binding-mcp/NOTICE.template b/runtime/binding-mcp/NOTICE.template new file mode 100644 index 0000000000..209ca12f74 --- /dev/null +++ b/runtime/binding-mcp/NOTICE.template @@ -0,0 +1,13 @@ +Licensed under the Aklivity Community License (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + https://www.aklivity.io/aklivity-community-license/ + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +This project includes: +#GENERATED_NOTICES# diff --git a/runtime/binding-mcp/mvnw b/runtime/binding-mcp/mvnw new file mode 100755 index 0000000000..d2f0ea3808 --- /dev/null +++ b/runtime/binding-mcp/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/runtime/binding-mcp/mvnw.cmd b/runtime/binding-mcp/mvnw.cmd new file mode 100644 index 0000000000..b26ab24f03 --- /dev/null +++ b/runtime/binding-mcp/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/runtime/binding-mcp/pom.xml b/runtime/binding-mcp/pom.xml new file mode 100644 index 0000000000..a28b9e72ee --- /dev/null +++ b/runtime/binding-mcp/pom.xml @@ -0,0 +1,222 @@ + + + + 4.0.0 + + io.aklivity.zilla + runtime + develop-SNAPSHOT + ../pom.xml + + + binding-mcp + zilla::runtime::binding-mcp + + + + Aklivity Community License Agreement + https://www.aklivity.io/aklivity-community-license/ + repo + + + + + 0.00 + 500 + + + + + ${project.groupId} + binding-mcp.spec + ${project.version} + provided + + + ${project.groupId} + engine + ${project.version} + provided + + + ${project.groupId} + engine + test-jar + ${project.version} + test + + + junit + junit + test + + + org.hamcrest + hamcrest + test + + + org.mockito + mockito-core + test + + + io.aklivity.k3po + control-junit + test + + + io.aklivity.k3po + lang + test + + + ${project.groupId} + binding-http.spec + ${project.version} + provided + + + + + + + org.jasig.maven + maven-notice-plugin + + + ${project.groupId} + flyweight-maven-plugin + ${project.version} + + core internal http mcp + io.aklivity.zilla.runtime.binding.mcp.internal.types + + + + + generate + + + + + + com.mycila + license-maven-plugin + + + maven-checkstyle-plugin + + + maven-dependency-plugin + + + process-resources + + unpack + + + + + ${project.groupId} + binding-mcp.spec + + + ^\Qio/aklivity/zilla/specs/binding/mcp/\E + io/aklivity/zilla/runtime/binding/mcp/internal/ + + + + + io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json + ${project.build.directory}/classes + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.moditect + moditect-maven-plugin + + + org.apache.maven.plugins + maven-jar-plugin + + + + test-jar + + + + + + io.aklivity.k3po + k3po-maven-plugin + + + ${project.groupId} + engine + ${project.version} + test-jar + + + ${project.groupId} + engine + ${project.version} + + + ${project.groupId} + binding-mcp.spec + ${project.version} + + + ${project.groupId} + binding-http.spec + ${project.version} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.jacoco + jacoco-maven-plugin + + + io/aklivity/zilla/runtime/binding/mcp/internal/types/**/*.class + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + ${jacoco.coverage.ratio} + + + CLASS + MISSEDCOUNT + ${jacoco.missed.count} + + + + + + + + + diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpConditionConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpConditionConfig.java new file mode 100644 index 0000000000..426a7113c4 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpConditionConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.config; + +import java.util.function.Function; + +import io.aklivity.zilla.runtime.engine.config.ConditionConfig; + +public final class McpConditionConfig extends ConditionConfig +{ + public final String kind; + + public McpConditionConfig( + String kind) + { + this.kind = kind; + } + + public static McpConditionConfigBuilder builder() + { + return new McpConditionConfigBuilder<>(McpConditionConfig.class::cast); + } + + public static McpConditionConfigBuilder builder( + Function mapper) + { + return new McpConditionConfigBuilder<>(mapper); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpConditionConfigBuilder.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpConditionConfigBuilder.java new file mode 100644 index 0000000000..acf40bfd5c --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpConditionConfigBuilder.java @@ -0,0 +1,54 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.config; + +import java.util.function.Function; + +import io.aklivity.zilla.runtime.engine.config.ConditionConfig; +import io.aklivity.zilla.runtime.engine.config.ConfigBuilder; + +public final class McpConditionConfigBuilder extends ConfigBuilder> +{ + private final Function mapper; + + private String kind; + + public McpConditionConfigBuilder( + Function mapper) + { + this.mapper = mapper; + } + + public McpConditionConfigBuilder kind( + String kind) + { + this.kind = kind; + return this; + } + + @Override + @SuppressWarnings("unchecked") + protected Class> thisType() + { + return (Class>) getClass(); + } + + @Override + public T build() + { + return mapper.apply(new McpConditionConfig(kind)); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfig.java new file mode 100644 index 0000000000..52dbb20ae9 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfig.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.config; + +import java.util.List; +import java.util.function.Function; + +import io.aklivity.zilla.runtime.engine.config.OptionsConfig; + +public final class McpOptionsConfig extends OptionsConfig +{ + public final List prompts; + + public McpOptionsConfig( + List prompts) + { + this.prompts = prompts; + } + + public static McpOptionsConfigBuilder builder() + { + return new McpOptionsConfigBuilder<>(McpOptionsConfig.class::cast); + } + + public static McpOptionsConfigBuilder builder( + Function mapper) + { + return new McpOptionsConfigBuilder<>(mapper); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfigBuilder.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfigBuilder.java new file mode 100644 index 0000000000..a43dd5676a --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpOptionsConfigBuilder.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.config; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +import io.aklivity.zilla.runtime.engine.config.ConfigBuilder; +import io.aklivity.zilla.runtime.engine.config.OptionsConfig; + +public final class McpOptionsConfigBuilder extends ConfigBuilder> +{ + private final Function mapper; + + private List prompts; + + public McpOptionsConfigBuilder( + Function mapper) + { + this.mapper = mapper; + } + + public McpOptionsConfigBuilder prompt( + String name, + String description) + { + if (prompts == null) + { + prompts = new ArrayList<>(); + } + prompts.add(new McpPromptConfig(name, description)); + return this; + } + + @Override + @SuppressWarnings("unchecked") + protected Class> thisType() + { + return (Class>) getClass(); + } + + @Override + public T build() + { + return mapper.apply(new McpOptionsConfig(prompts)); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpPromptConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpPromptConfig.java new file mode 100644 index 0000000000..c2b32e31d8 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/config/McpPromptConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.config; + +public final class McpPromptConfig +{ + public final String name; + public final String description; + + public McpPromptConfig( + String name, + String description) + { + this.name = name; + this.description = description; + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBinding.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBinding.java new file mode 100644 index 0000000000..b8c52f575d --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBinding.java @@ -0,0 +1,53 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal; + +import java.net.URL; + +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.binding.Binding; + +public final class McpBinding implements Binding +{ + public static final String NAME = "mcp"; + + private final McpConfiguration config; + + McpBinding( + McpConfiguration config) + { + this.config = config; + } + + @Override + public String name() + { + return McpBinding.NAME; + } + + @Override + public URL type() + { + return getClass().getResource("schema/mcp.schema.patch.json"); + } + + @Override + public McpBindingContext supply( + EngineContext context) + { + return new McpBindingContext(config, context); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBindingContext.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBindingContext.java new file mode 100644 index 0000000000..b7177bb45a --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBindingContext.java @@ -0,0 +1,73 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal; + +import static io.aklivity.zilla.runtime.engine.config.KindConfig.SERVER; +import static java.util.Collections.singletonMap; + +import java.util.Map; + +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.McpServerFactory; +import io.aklivity.zilla.runtime.binding.mcp.internal.stream.McpStreamFactory; +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.binding.BindingContext; +import io.aklivity.zilla.runtime.engine.binding.BindingHandler; +import io.aklivity.zilla.runtime.engine.config.BindingConfig; +import io.aklivity.zilla.runtime.engine.config.KindConfig; + +final class McpBindingContext implements BindingContext +{ + private final Map factories; + + McpBindingContext( + McpConfiguration config, + EngineContext context) + { + this.factories = singletonMap(SERVER, new McpServerFactory(config, context)); + } + + @Override + public BindingHandler attach( + BindingConfig binding) + { + final McpStreamFactory factory = factories.get(binding.kind); + + if (factory != null) + { + factory.attach(binding); + } + + return factory; + } + + @Override + public void detach( + BindingConfig binding) + { + final McpStreamFactory factory = factories.get(binding.kind); + + if (factory != null) + { + factory.detach(binding.id); + } + } + + @Override + public String toString() + { + return String.format("%s %s", getClass().getSimpleName(), factories); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBindingFactorySpi.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBindingFactorySpi.java new file mode 100644 index 0000000000..8a2a4a98fb --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpBindingFactorySpi.java @@ -0,0 +1,35 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal; + +import io.aklivity.zilla.runtime.engine.Configuration; +import io.aklivity.zilla.runtime.engine.binding.BindingFactorySpi; + +public final class McpBindingFactorySpi implements BindingFactorySpi +{ + @Override + public String type() + { + return McpBinding.NAME; + } + + @Override + public McpBinding create( + Configuration config) + { + return new McpBinding(new McpConfiguration(config)); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfiguration.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfiguration.java new file mode 100644 index 0000000000..e94b3ec028 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfiguration.java @@ -0,0 +1,103 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal; + +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.UUID; +import java.util.function.Supplier; + +import org.agrona.LangUtil; + +import io.aklivity.zilla.runtime.engine.Configuration; + +public class McpConfiguration extends Configuration +{ + private static final ConfigurationDef MCP_CONFIG; + + public static final PropertyDef MCP_SESSION_ID; + + static + { + final ConfigurationDef config = new ConfigurationDef("zilla.binding.mcp"); + MCP_SESSION_ID = config.property(SessionIdSupplier.class, "session.id", + McpConfiguration::decodeSessionIdSupplier, McpConfiguration::defaultSessionIdSupplier); + MCP_CONFIG = config; + } + + public McpConfiguration() + { + super(MCP_CONFIG, new Configuration()); + } + + public McpConfiguration( + Configuration config) + { + super(MCP_CONFIG, config); + } + + public Supplier sessionIdSupplier() + { + return MCP_SESSION_ID.get(this)::get; + } + + @FunctionalInterface + public interface SessionIdSupplier + { + String get(); + } + + private static SessionIdSupplier decodeSessionIdSupplier( + String value) + { + SessionIdSupplier supplier = null; + + try + { + MethodType signature = MethodType.methodType(String.class); + String[] parts = value.split("::"); + Class ownerClass = Class.forName(parts[0]); + String methodName = parts[1]; + MethodHandle method = MethodHandles.publicLookup().findStatic(ownerClass, methodName, signature); + supplier = () -> + { + String sessionId = null; + try + { + sessionId = (String) method.invoke(); + } + catch (Throwable ex) + { + LangUtil.rethrowUnchecked(ex); + } + + return sessionId; + }; + } + catch (Throwable ex) + { + LangUtil.rethrowUnchecked(ex); + } + + return supplier; + } + + private static String defaultSessionIdSupplier() + { + return UUID.randomUUID().toString(); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpClientCapabilities.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpClientCapabilities.java new file mode 100644 index 0000000000..d5c52b8596 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpClientCapabilities.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.codec; + +import jakarta.json.JsonObject; + +public class McpClientCapabilities +{ + public JsonObject experimental; + public Roots roots; + public Sampling sampling; + public Elicitation elicitation; + public Tasks tasks; + + public class Roots + { + public boolean listChanged; + } + + public class Sampling + { + public Context context; + public Tools tools; + + public record Context() + { + } + + public record Tools() + { + } + } + + public class Elicitation + { + public Form form; + public Url url; + + public record Form() + { + } + + public record Url() + { + } + } + + public class Tasks + { + public List list; + public Cancel cancel; + public Requests requests; + + public record List() + { + } + + public record Cancel() + { + } + + public class Requests + { + public Sampling sampling; + public Elicitation elicitation; + + public class Sampling + { + public CreateMessage createMessage; + + public record CreateMessage() + { + } + } + + public class Elicitation + { + public Create create; + + public record Create() + { + } + } + } + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpImplementationInfo.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpImplementationInfo.java new file mode 100644 index 0000000000..b1eefd41e4 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpImplementationInfo.java @@ -0,0 +1,22 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.codec; + +public class McpImplementationInfo +{ + public String name; + public String version; +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpInitializeRequestParams.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpInitializeRequestParams.java new file mode 100644 index 0000000000..7ec56f8aa5 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpInitializeRequestParams.java @@ -0,0 +1,23 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.codec; + +public class McpInitializeRequestParams +{ + public String protocolVersion; + public McpClientCapabilities capabilities; + public McpImplementationInfo clientInfo; +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpNotifyCanceledParams.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpNotifyCanceledParams.java new file mode 100644 index 0000000000..c2e3202a59 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpNotifyCanceledParams.java @@ -0,0 +1,24 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.codec; + +import jakarta.json.JsonValue; + +public class McpNotifyCanceledParams +{ + public JsonValue requestId; + public String reason; +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpServerCapabilities.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpServerCapabilities.java new file mode 100644 index 0000000000..eed73e6206 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/codec/McpServerCapabilities.java @@ -0,0 +1,78 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.codec; + +import java.util.Map; +import java.util.Optional; + +public record McpServerCapabilities( + Optional> experimental, + Optional logging, + Optional completions, + Optional prompts, + Optional resources, + Optional tools) +{ + public record Logging() + { + } + + public record Completions() + { + } + + public record Prompts( + boolean listChanged) + { + } + + public record Resources( + boolean subscribe, + boolean listChanged) + { + } + + public record Tools( + boolean listChanged) + { + } + + public record Tasks( + Optional list, + Optional cancel, + Optional requests) + { + public record List() + { + } + + public record Cancel() + { + } + + public record Requests( + Optional tools) + { + public record Tools( + Optional call) + { + public record Call() + { + } + } + } + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpBindingConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpBindingConfig.java new file mode 100644 index 0000000000..0c88eb7ef9 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpBindingConfig.java @@ -0,0 +1,49 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.config; + +import java.util.List; +import java.util.stream.Collectors; + +import io.aklivity.zilla.runtime.binding.mcp.config.McpOptionsConfig; +import io.aklivity.zilla.runtime.engine.config.BindingConfig; + +public final class McpBindingConfig +{ + public final long id; + public final McpOptionsConfig options; + + private final List routes; + + public McpBindingConfig( + BindingConfig binding) + { + this.id = binding.id; + this.options = (McpOptionsConfig) binding.options; + this.routes = binding.routes.stream() + .map(McpRouteConfig::new) + .collect(Collectors.toList()); + } + + public McpRouteConfig resolve( + long authorization) + { + return routes.stream() + .filter(r -> r.authorized(authorization) && r.matches("")) + .findFirst() + .orElse(null); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpConditionConfigAdapter.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpConditionConfigAdapter.java new file mode 100644 index 0000000000..a3c4a61b21 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpConditionConfigAdapter.java @@ -0,0 +1,66 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.config; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.bind.adapter.JsonbAdapter; + +import io.aklivity.zilla.runtime.binding.mcp.config.McpConditionConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.McpBinding; +import io.aklivity.zilla.runtime.engine.config.ConditionConfig; +import io.aklivity.zilla.runtime.engine.config.ConditionConfigAdapterSpi; + +public final class McpConditionConfigAdapter implements ConditionConfigAdapterSpi, JsonbAdapter +{ + private static final String KIND_NAME = "kind"; + + @Override + public String type() + { + return McpBinding.NAME; + } + + @Override + public JsonObject adaptToJson( + ConditionConfig condition) + { + McpConditionConfig mcpCondition = (McpConditionConfig) condition; + + JsonObjectBuilder object = Json.createObjectBuilder(); + + if (mcpCondition.kind != null) + { + object.add(KIND_NAME, mcpCondition.kind); + } + + return object.build(); + } + + @Override + public ConditionConfig adaptFromJson( + JsonObject object) + { + String kind = object.containsKey(KIND_NAME) + ? object.getString(KIND_NAME) + : null; + + return McpConditionConfig.builder() + .kind(kind) + .build(); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpOptionsConfigAdapter.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpOptionsConfigAdapter.java new file mode 100644 index 0000000000..65b031cf7f --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpOptionsConfigAdapter.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.config; + +import jakarta.json.Json; +import jakarta.json.JsonArray; +import jakarta.json.JsonArrayBuilder; +import jakarta.json.JsonObject; +import jakarta.json.JsonObjectBuilder; +import jakarta.json.bind.adapter.JsonbAdapter; + +import io.aklivity.zilla.runtime.binding.mcp.config.McpOptionsConfig; +import io.aklivity.zilla.runtime.binding.mcp.config.McpOptionsConfigBuilder; +import io.aklivity.zilla.runtime.binding.mcp.config.McpPromptConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.McpBinding; +import io.aklivity.zilla.runtime.engine.config.OptionsConfig; +import io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi; + +public final class McpOptionsConfigAdapter implements OptionsConfigAdapterSpi, JsonbAdapter +{ + private static final String PROMPTS_NAME = "prompts"; + private static final String PROMPT_NAME_NAME = "name"; + private static final String PROMPT_DESCRIPTION_NAME = "description"; + + @Override + public Kind kind() + { + return Kind.BINDING; + } + + @Override + public String type() + { + return McpBinding.NAME; + } + + @Override + public JsonObject adaptToJson( + OptionsConfig options) + { + McpOptionsConfig mcpOptions = (McpOptionsConfig) options; + + JsonObjectBuilder object = Json.createObjectBuilder(); + + if (mcpOptions.prompts != null && !mcpOptions.prompts.isEmpty()) + { + JsonArrayBuilder prompts = Json.createArrayBuilder(); + for (McpPromptConfig prompt : mcpOptions.prompts) + { + JsonObjectBuilder promptObject = Json.createObjectBuilder(); + promptObject.add(PROMPT_NAME_NAME, prompt.name); + if (prompt.description != null) + { + promptObject.add(PROMPT_DESCRIPTION_NAME, prompt.description); + } + prompts.add(promptObject); + } + object.add(PROMPTS_NAME, prompts); + } + + return object.build(); + } + + @Override + public OptionsConfig adaptFromJson( + JsonObject object) + { + McpOptionsConfigBuilder builder = McpOptionsConfig.builder(); + + if (object.containsKey(PROMPTS_NAME)) + { + JsonArray prompts = object.getJsonArray(PROMPTS_NAME); + for (int i = 0; i < prompts.size(); i++) + { + JsonObject prompt = prompts.getJsonObject(i); + String name = prompt.getString(PROMPT_NAME_NAME); + String description = prompt.containsKey(PROMPT_DESCRIPTION_NAME) + ? prompt.getString(PROMPT_DESCRIPTION_NAME) + : null; + builder.prompt(name, description); + } + } + + return builder.build(); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpRouteConfig.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpRouteConfig.java new file mode 100644 index 0000000000..0c34728741 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/config/McpRouteConfig.java @@ -0,0 +1,50 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.config; + +import java.util.List; +import java.util.stream.Collectors; + +import io.aklivity.zilla.runtime.binding.mcp.config.McpConditionConfig; +import io.aklivity.zilla.runtime.engine.config.RouteConfig; + +public final class McpRouteConfig +{ + public final long id; + private final List when; + + public McpRouteConfig( + RouteConfig route) + { + this.id = route.id; + this.when = route.when.stream() + .map(McpConditionConfig.class::cast) + .collect(Collectors.toList()); + } + + public boolean authorized( + long authorization) + { + return true; + } + + public boolean matches( + String kind) + { + return when.isEmpty() || + when.stream().anyMatch(c -> c.kind == null || c.kind.equals(kind)); + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerFactory.java new file mode 100644 index 0000000000..df71b48187 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerFactory.java @@ -0,0 +1,2791 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static io.aklivity.zilla.runtime.engine.buffer.BufferPool.NO_SLOT; + +import java.util.Map; +import java.util.function.LongUnaryOperator; +import java.util.function.Supplier; + +import jakarta.json.Json; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.stream.JsonParser; + +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.collections.Long2ObjectHashMap; +import org.agrona.collections.Object2ObjectHashMap; +import org.agrona.concurrent.UnsafeBuffer; +import org.agrona.io.DirectBufferInputStream; + +import io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration; +import io.aklivity.zilla.runtime.binding.mcp.internal.codec.McpNotifyCanceledParams; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpBindingConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.config.McpRouteConfig; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.Flyweight; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.HttpHeaderFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.OctetsFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.String8FW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.AbortFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.BeginFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.DataFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.EndFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.FlushFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.HttpBeginExFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.HttpResetExFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.ResetFW; +import io.aklivity.zilla.runtime.binding.mcp.internal.types.stream.WindowFW; +import io.aklivity.zilla.runtime.engine.EngineContext; +import io.aklivity.zilla.runtime.engine.binding.BindingHandler; +import io.aklivity.zilla.runtime.engine.binding.function.MessageConsumer; +import io.aklivity.zilla.runtime.engine.buffer.BufferPool; +import io.aklivity.zilla.runtime.engine.config.BindingConfig; + +public final class McpServerFactory implements McpStreamFactory +{ + private static final byte START_OBJECT_BYTE = (byte) '{'; + private static final String HTTP_TYPE_NAME = "http"; + private static final String MCP_TYPE_NAME = "mcp"; + + private static final String JSON_RPC_VERSION = "2.0"; + private static final String HTTP_HEADER_METHOD = ":method"; + private static final String HTTP_HEADER_SESSION = "mcp-session-id"; + private static final String HTTP_HEADER_STATUS = ":status"; + private static final String HTTP_HEADER_CONTENT_TYPE = "content-type"; + private static final String CONTENT_TYPE_JSON = "application/json"; + private static final String STATUS_200 = "200"; + private static final String STATUS_202 = "202"; + private static final String STATUS_400 = "400"; + private static final String STATUS_405 = "405"; + + private final BeginFW beginRO = new BeginFW(); + private final DataFW dataRO = new DataFW(); + private final EndFW endRO = new EndFW(); + private final AbortFW abortRO = new AbortFW(); + private final FlushFW flushRO = new FlushFW(); + private final WindowFW windowRO = new WindowFW(); + private final ResetFW resetRO = new ResetFW(); + private final HttpBeginExFW httpBeginExRO = new HttpBeginExFW(); + private final OctetsFW emptyRO = new OctetsFW().wrap(new UnsafeBuffer(), 0, 0); + + private final BeginFW.Builder beginRW = new BeginFW.Builder(); + private final DataFW.Builder dataRW = new DataFW.Builder(); + private final EndFW.Builder endRW = new EndFW.Builder(); + private final AbortFW.Builder abortRW = new AbortFW.Builder(); + private final FlushFW.Builder flushRW = new FlushFW.Builder(); + private final WindowFW.Builder windowRW = new WindowFW.Builder(); + private final ResetFW.Builder resetRW = new ResetFW.Builder(); + private final HttpBeginExFW.Builder httpBeginExRW = new HttpBeginExFW.Builder(); + private final HttpResetExFW.Builder httpResetExRW = new HttpResetExFW.Builder(); + private final McpBeginExFW.Builder mcpBeginExRW = new McpBeginExFW.Builder(); + + private final Supplier supplySessionId; + private final MutableDirectBuffer writeBuffer; + private final MutableDirectBuffer codecBuffer; + private final BindingHandler streamFactory; + private final LongUnaryOperator supplyInitialId; + private final LongUnaryOperator supplyReplyId; + private final int httpTypeId; + private final int mcpTypeId; + private final BufferPool decodePool; + private final BufferPool encodePool; + private final int decodeMax; + private final int encodeMax; + + private final DirectBufferInputStream inputRO = new DirectBufferInputStream(); + + private final McpServerDecoder decodeJsonRpc = this::decodeJsonRpc; + private final McpServerDecoder decodeJsonRpcStart = this::decodeJsonRpcStart; + private final McpServerDecoder decodeJsonRpcNext = this::decodeJsonRpcNext; + private final McpServerDecoder decodeJsonRpcEnd = this::decodeJsonRpcEnd; + private final McpServerDecoder decodeJsonRpcVersion = this::decodeJsonRpcVersion; + private final McpServerDecoder decodeJsonRpcId = this::decodeJsonRpcId; + private final McpServerDecoder decodeJsonRpcMethod = this::decodeJsonRpcMethod; + private final McpServerDecoder decodeJsonRpcParamsStart = this::decodeJsonRpcParamsStart; + private final McpServerDecoder decodeJsonRpcParamsEnd = this::decodeJsonRpcParamsEnd; + private final McpServerDecoder decodeJsonRpcMethodWithParam = this::decodeJsonRpcMethodWithParam; + private final McpServerDecoder decodeJsonRpcMethodParamValue = this::decodeJsonRpcMethodParamValue; + private final McpServerDecoder decodeJsonRpcParamsSkipValue = this::decodeJsonRpcParamsSkipValue; + private final McpServerDecoder decodeIgnore = this::decodeIgnore; + + private final Long2ObjectHashMap bindings; + private final Map sessions; + + public McpServerFactory( + McpConfiguration config, + EngineContext context) + { + this.supplySessionId = config.sessionIdSupplier(); + this.writeBuffer = context.writeBuffer(); + this.codecBuffer = new UnsafeBuffer(new byte[context.writeBuffer().capacity()]); + this.streamFactory = context.streamFactory(); + this.supplyInitialId = context::supplyInitialId; + this.supplyReplyId = context::supplyReplyId; + this.bindings = new Long2ObjectHashMap<>(); + this.httpTypeId = context.supplyTypeId(HTTP_TYPE_NAME); + this.mcpTypeId = context.supplyTypeId(MCP_TYPE_NAME); + this.decodePool = context.bufferPool(); + this.encodePool = context.bufferPool().duplicate(); + this.decodeMax = decodePool.slotCapacity(); + this.encodeMax = encodePool.slotCapacity(); + this.sessions = new Object2ObjectHashMap<>(); + } + + @Override + public int originTypeId() + { + return httpTypeId; + } + + @Override + public int routedTypeId() + { + return mcpTypeId; + } + + @Override + public void attach( + BindingConfig binding) + { + McpBindingConfig newBinding = new McpBindingConfig(binding); + bindings.put(binding.id, newBinding); + } + + @Override + public void detach( + long bindingId) + { + bindings.remove(bindingId); + } + + @Override + public MessageConsumer newStream( + int msgTypeId, + DirectBuffer buffer, + int index, + int length, + MessageConsumer sender) + { + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + final long originId = begin.originId(); + final long routedId = begin.routedId(); + final long initialId = begin.streamId(); + final long authorization = begin.authorization(); + + MessageConsumer newStream = null; + + final McpBindingConfig binding = bindings.get(routedId); + final McpRouteConfig route = binding != null ? binding.resolve(authorization) : null; + + if (route != null) + { + final long resolvedId = route.id; + + final long initialSeq = begin.sequence(); + final long initialAck = begin.acknowledge(); + final long traceId = begin.traceId(); + final OctetsFW extension = begin.extension(); + final HttpBeginExFW httpBeginEx = extension.get(httpBeginExRO::wrap); + + McpLifecycleStream session = null; + + final HttpHeaderFW sessionHeader = httpBeginEx.headers() + .matchFirst(h -> HTTP_HEADER_SESSION.equals(h.name().asString())); + + if (sessionHeader != null) + { + String sessionId = sessionHeader.value().asString(); + session = sessions.get(sessionId); + } + + final HttpHeaderFW methodHeader = httpBeginEx.headers() + .matchFirst(h -> HTTP_HEADER_METHOD.equals(h.name().asString())); + + String method = methodHeader.value().asString(); + + newStream = switch (method) + { + case "POST" -> new McpServer( + sender, + originId, + routedId, + initialId, + resolvedId, + session)::onNetMessage; + case "DELETE" -> new McpShutdownHandler( + sender, + originId, + routedId, + initialId, + resolvedId, + session)::onNetMessage; + default -> (t, b, o, l) -> + doReset(sender, originId, routedId, initialId, initialSeq, initialAck, + 0, traceId, authorization, httpResetExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_405)) + .build()); + }; + } + + return newStream; + } + + @FunctionalInterface + private interface McpServerDecoder + { + int decode( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit); + } + + @FunctionalInterface + private interface McpServerRequestParamsConsumer + { + int accept( + long traceId, + long authorization, + DirectBuffer buffer, + int offset, + int limit); + } + + private int decodeJsonRpc( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + DirectBufferInputStream input = inputRO; + input.wrap(buffer, progress, limit - progress); + + server.decodableJson = Json.createParser(input); + server.decoder = decodeJsonRpcStart; + + progress = limit - input.available(); + + return progress; + } + + private int decodeJsonRpcStart( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + DirectBufferInputStream input = inputRO; + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + if (event != JsonParser.Event.START_OBJECT) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + server.decoder = decodeJsonRpcNext; + + progress = limit - input.available(); + } + + return progress; + } + + private int decodeJsonRpcNext( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + DirectBufferInputStream input = inputRO; + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + switch (event) + { + case JsonParser.Event.KEY_NAME: + final String key = parser.getString(); + switch (key) + { + case "jsonrpc": + server.decoder = decodeJsonRpcVersion; + break; + case "id": + server.decoder = decodeJsonRpcId; + break; + case "method": + server.decoder = decodeJsonRpcMethod; + break; + case "params": + if (server.decodedMethod == null) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + server.decoder = decodeJsonRpcParamsStart; + break; + default: + server.onDecodeInvalidRequest(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + break; + case JsonParser.Event.END_OBJECT: + server.decoder = decodeJsonRpcEnd; + break; + default: + server.onDecodeParseError(traceId, authorization); + break decode; + } + + progress = limit - input.available(); + } + + return progress; + } + + private int decodeJsonRpcEnd( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + DirectBufferInputStream input = inputRO; + JsonParser parser = server.decodableJson; + + decode: + { + if (parser.hasNext()) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + parser.close(); + server.decoder = decodeIgnore; + + progress = limit - input.available(); + } + + return progress; + } + + private int decodeJsonRpcVersion( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + DirectBufferInputStream input = inputRO; + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + if (event != JsonParser.Event.VALUE_STRING || + !JSON_RPC_VERSION.equals(parser.getString())) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + server.decoder = decodeJsonRpcNext; + + progress = limit - input.available(); + } + + return progress; + } + + private int decodeJsonRpcId( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + DirectBufferInputStream input = inputRO; + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + if (event != JsonParser.Event.VALUE_STRING && + event != JsonParser.Event.VALUE_NUMBER) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + final String id = parser.getString(); + server.decodedId = id; + server.decoder = decodeJsonRpcNext; + + progress = limit - input.available(); + } + + return progress; + } + + private int decodeJsonRpcMethod( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + DirectBufferInputStream input = inputRO; + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + if (event != JsonParser.Event.VALUE_STRING) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + final String method = parser.getString(); + if ("initialize".equals(method)) + { + server.decodedRequest = server::onDecodeInitialize; + } + else + { + if (server.session == null) + { + server.onDecodeInvalidRequest(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + switch (method) + { + case "notifications/initialized": + server.onDecodeNotifyInitialized(traceId, authorization); + break; + case "ping": + server.onDecodePing(traceId, authorization); + break; + case "tools/list": + server.onDecodeToolsList(traceId, authorization); + server.decodedRequest = server::onDecodeRequestParams; + break; + case "tools/call": + server.decodedMethodParam = "name"; + server.decodedRequest = server::onDecodeRequestParams; + break; + case "prompts/list": + server.onDecodePromptsList(traceId, authorization); + server.decodedRequest = server::onDecodeRequestParams; + break; + case "prompts/get": + server.decodedMethodParam = "name"; + server.decodedRequest = server::onDecodeRequestParams; + break; + case "resources/list": + server.onDecodeResourcesList(traceId, authorization); + server.decodedRequest = server::onDecodeRequestParams; + break; + case "resources/read": + server.decodedMethodParam = "uri"; + server.decodedRequest = server::onDecodeRequestParams; + break; + case "notifications/cancelled": + server.decodedRequest = server::onDecodeNotifyCancelled; + break; + default: + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + } + + server.decodedMethod = method; + server.decoder = decodeJsonRpcNext; + + progress = limit - input.available(); + } + + return progress; + } + + private int decodeJsonRpcParamsStart( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final int startObjectOffset = (int) parser.getLocation().getStreamOffset(); + + final JsonParser.Event event = parser.next(); + if (event != JsonParser.Event.START_OBJECT) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + final int startObjectLimit = (int) parser.getLocation().getStreamOffset(); + server.decodedParamsParsed = + indexOfByte(buffer, offset + startObjectOffset, offset + startObjectLimit, START_OBJECT_BYTE) - offset; + + if (server.decodedMethodParam != null) + { + server.decoder = decodeJsonRpcMethodWithParam; + } + else + { + parser.skipObject(); // TODO: non-blocking + server.decoder = decodeJsonRpcParamsEnd; + } + + progress = offset + server.decodedParamsParsed - server.decodeParserProgress; + } + + return progress; + } + + private int decodeJsonRpcMethodWithParam( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + if (event != JsonParser.Event.KEY_NAME) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + final String key = parser.getString(); + if (server.decodedMethodParam.equals(key)) + { + server.decoder = decodeJsonRpcMethodParamValue; + } + else + { + server.decoder = decodeJsonRpcParamsSkipValue; + } + } + + progress = offset + server.decodedParamsParsed - server.decodeParserProgress; + + return progress; + } + + private int decodeJsonRpcParamsSkipValue( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + switch (event) + { + case JsonParser.Event.START_OBJECT: + parser.skipObject(); // TODO: non-blocking + break; + case JsonParser.Event.START_ARRAY: + parser.skipArray(); // TODO: non-blocking + break; + default: + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + server.decoder = decodeJsonRpcMethodWithParam; + } + + progress = offset + server.decodedParamsParsed - server.decodeParserProgress; + + return progress; + } + + private int decodeJsonRpcMethodParamValue( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + JsonParser parser = server.decodableJson; + + decode: + if (parser.hasNext()) + { + final JsonParser.Event event = parser.next(); + if (event != JsonParser.Event.VALUE_STRING) + { + server.onDecodeParseError(traceId, authorization); + server.decoder = decodeIgnore; + break decode; + } + + final String value = parser.getString(); + switch (server.decodedMethod) + { + case "tools/call": + server.onDecodeToolsCall(value, traceId, authorization); + break; + case "prompts/get": + server.onDecodePromptsGet(value, traceId, authorization); + break; + case "resources/read": + server.onDecodeResourcesRead(value, traceId, authorization); + break; + } + + parser.skipObject(); // TODO: non-blocking + server.decoder = decodeJsonRpcParamsEnd; + + progress = offset + server.decodedParamsParsed - server.decodeParserProgress; + } + + return progress; + } + + private int decodeJsonRpcParamsEnd( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + JsonParser parser = server.decodableJson; + + final int decodedParamsParsed = (int) parser.getLocation().getStreamOffset(); + if (decodedParamsParsed > server.decodedParamsParsed) + { + final int decodedOffset = offset + server.decodedParamsParsed - server.decodeParserProgress; + final int decodedLimit = offset + decodedParamsParsed - server.decodeParserProgress; + final int decodedProgress = + server.decodedRequest.accept(traceId, authorization, buffer, decodedOffset, decodedLimit); + + server.decodedParamsParsed = decodedProgress - offset + server.decodeParserProgress; + } + + progress = offset + server.decodedParamsParsed - server.decodeParserProgress; + + if (server.decodedParamsParsed == decodedParamsParsed) + { + server.decoder = decodeJsonRpcNext; + } + + return progress; + } + + private int decodeIgnore( + McpServer server, + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int progress, + int limit) + { + return limit; + } + + private final class McpServer + { + private final MessageConsumer net; + private final long originId; + private final long routedId; + private final long initialId; + private final long replyId; + private final long resolvedId; + + private McpLifecycleStream session; + + private long initialSeq; + private long initialAck; + private int initialMax; + + private long replySeq; + private long replyAck; + private int replyMax; + private long replyBud; + private int replyPad; + + private int state; + + private int decodeSlot = BufferPool.NO_SLOT; + private int decodeSlotOffset; + private int decodeSlotReserved; + private int decodeParserProgress; + + private int encodeSlot = BufferPool.NO_SLOT; + private int encodeSlotOffset; + private long encodeSlotTraceId; + + private McpServerDecoder decoder; + + private JsonParser decodableJson; + private String decodedMethod; + private String decodedMethodParam; + private String decodedId; + private int decodedParamsParsed; + private McpServerRequestParamsConsumer decodedRequest; + + private McpRequestStream stream; + + private McpServer( + MessageConsumer sender, + long originId, + long routedId, + long initialId, + long resolvedId, + McpLifecycleStream session) + { + this.net = sender; + this.originId = originId; + this.routedId = routedId; + this.initialId = initialId; + this.replyId = supplyReplyId.applyAsLong(initialId); + this.resolvedId = resolvedId; + this.session = session; + this.decoder = decodeJsonRpc; + } + + private void onNetMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onNetBegin(begin); + break; + case DataFW.TYPE_ID: + final DataFW data = dataRO.wrap(buffer, index, index + length); + onNetData(data); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onNetEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onNetAbort(abort); + break; + case FlushFW.TYPE_ID: + final FlushFW flush = flushRO.wrap(buffer, index, index + length); + onNetFlush(flush); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onNetWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onNetReset(reset); + break; + default: + break; + } + } + + private void onNetBegin( + BeginFW begin) + { + final long traceId = begin.traceId(); + final long authorization = begin.authorization(); + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final int maximum = begin.maximum(); + + initialSeq = sequence; + initialAck = acknowledge; + initialMax = maximum; + + state = McpState.openedInitial(state); + + doNetWindow(traceId, authorization, 0, 0); + } + + private void onNetData( + DataFW data) + { + final long sequence = data.sequence(); + final long acknowledge = data.acknowledge(); + final long traceId = data.traceId(); + final long authorization = data.authorization(); + final long budgetId = data.budgetId(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence + data.reserved(); + + assert initialAck <= initialSeq; + + if (initialSeq > initialAck + decodeMax) + { + cleanupNet(traceId, authorization); + } + else + { + final OctetsFW payload = data.payload(); + int reserved = data.reserved(); + DirectBuffer buffer = payload.buffer(); + int offset = payload.offset(); + int limit = payload.limit(); + + if (decodeSlot != NO_SLOT) + { + final MutableDirectBuffer slotBuffer = decodePool.buffer(decodeSlot); + slotBuffer.putBytes(decodeSlotOffset, buffer, offset, limit - offset); + decodeSlotOffset += limit - offset; + decodeSlotReserved += reserved; + + buffer = slotBuffer; + offset = 0; + limit = decodeSlotOffset; + reserved = decodeSlotReserved; + } + + if (decodableJson != null) + { + // TODO: check fragmentation scenario + final int delta = (int) (decodableJson.getLocation().getStreamOffset() - decodeParserProgress); + final DirectBufferInputStream input = inputRO; + input.wrap(buffer, offset + delta, limit - offset - delta); + } + + decodeNet(traceId, authorization, budgetId, reserved, buffer, offset, limit); + } + } + + private void onNetEnd( + EndFW end) + { + final long traceId = end.traceId(); + final long authorization = end.authorization(); + + state = McpState.closingInitial(state); + + if (decodeSlot == BufferPool.NO_SLOT && + stream != null) + { + state = McpState.closedInitial(state); + stream.doAppEnd(traceId, authorization); + } + } + + private void onNetAbort( + AbortFW abort) + { + final long traceId = abort.traceId(); + final long authorization = abort.authorization(); + + cleanupDecodeSlot(); + + if (stream != null) + { + stream.doAppAbort(traceId, authorization); + } + } + + private void onNetFlush( + FlushFW flush) + { + final long sequence = flush.sequence(); + final long acknowledge = flush.acknowledge(); + final long traceId = flush.traceId(); + final long authorization = flush.authorization(); + final long budgetId = flush.budgetId(); + final int reserved = flush.reserved(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence + reserved; + + assert initialAck <= initialSeq; + + if (initialSeq > initialAck + decodeMax) + { + cleanupNet(traceId, authorization); + } + else if (stream != null) + { + stream.doAppFlush(traceId, authorization, budgetId, reserved); + } + } + + private void onNetWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final int maximum = window.maximum(); + final long traceId = window.traceId(); + final long authorization = window.authorization(); + final long budgetId = window.budgetId(); + final int padding = window.padding(); + + assert acknowledge <= sequence; + assert sequence <= replySeq; + assert acknowledge >= replyAck; + assert maximum >= replyMax; + + replyAck = acknowledge; + replyMax = maximum; + replyBud = budgetId; + replyPad = padding; + + assert replyAck <= replySeq; + + if (encodeSlot != NO_SLOT) + { + final MutableDirectBuffer buffer = encodePool.buffer(encodeSlot); + final int limit = encodeSlotOffset; + + encodeNet(encodeSlotTraceId, budgetId, buffer, 0, limit); + } + + if (stream != null) + { + stream.flushAppWindow(traceId, authorization, 0L, 0, encodeMax - encodeSlotOffset, encodeMax); + } + } + + private void onNetReset( + ResetFW reset) + { + final long traceId = reset.traceId(); + final long authorization = reset.authorization(); + + if (stream != null) + { + stream.doAppAbort(traceId, authorization); + } + + cleanupEncodeSlot(); + } + + private void doNetBegin( + long traceId, + long authorization, + Flyweight extension) + { + if (!McpState.replyOpening(state)) + { + doBegin(net, originId, routedId, replyId, + replySeq, replyAck, replyMax, traceId, authorization, 0, + extension); + + state = McpState.openingReply(state); + } + } + + private void doNetData( + long traceId, + long authorization, + DirectBuffer payload) + { + doNetData(traceId, authorization, payload, 0, payload.capacity()); + } + + private void doNetData( + long traceId, + long authorization, + Flyweight payload) + { + doNetData(traceId, authorization, payload.buffer(), payload.offset(), payload.limit()); + } + + private void doNetData( + long traceId, + long authorization, + DirectBuffer buffer, + int offset, + int limit) + { + if (encodeSlot != NO_SLOT) + { + final MutableDirectBuffer encodeBuffer = encodePool.buffer(encodeSlot); + encodeBuffer.putBytes(encodeSlotOffset, buffer, offset, limit - offset); + encodeSlotOffset += limit - offset; + encodeSlotTraceId = traceId; + + buffer = encodeBuffer; + offset = 0; + limit = encodeSlotOffset; + } + + encodeNet(traceId, authorization, buffer, offset, limit); + } + + private void doNetFlush( + long traceId, + long authorization, + long budgetId, + int reserved) + { + if (McpState.initialOpened(state)) + { + doFlush(net, originId, routedId, replyId, + replySeq, replyAck, replyMax, traceId, authorization, + budgetId, reserved); + } + } + + private void doNetEnd( + long traceId, + long authorization) + { + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + doEnd(net, originId, routedId, replyId, + replySeq, replyAck, replyMax, traceId, authorization); + } + + cleanupEncodeSlot(); + } + + private void doNetAbort( + long traceId, + long authorization) + { + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + doAbort(net, originId, routedId, replyId, + replySeq, replyAck, replyMax, traceId, authorization); + } + + cleanupEncodeSlot(); + } + + private void doNetWindow( + long traceId, + long authorization, + long budgetId, + int padding) + { + doWindow(net, originId, routedId, initialId, + initialSeq, initialAck, decodeMax - decodeSlotReserved, traceId, authorization, budgetId, padding); + } + + private void doNetReset( + long traceId, + long authorization) + { + doNetReset(traceId, authorization, emptyRO); + } + + private void doNetReset( + long traceId, + long authorization, + Flyweight extension) + { + if (!McpState.initialClosed(state)) + { + state = McpState.closedInitial(state); + doReset(net, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, authorization, extension); + } + } + + private void flushNetWindow( + long traceId, + long authorization, + long budgetId) + { + final long initialAckMax = Math.min(initialAck + decodeMax - decodeSlotReserved, initialSeq); + if (initialAckMax > initialAck) + { + initialAck = initialAckMax; + assert initialAck <= initialSeq; + + doNetWindow(traceId, authorization, budgetId, 0); + } + + decodeNet(traceId, authorization, budgetId); + } + + private void decodeNet( + long traceId, + long authorization, + long budgetId) + { + if (decodeSlot != NO_SLOT) + { + final MutableDirectBuffer buffer = decodePool.buffer(decodeSlot); + final int reserved = decodeSlotReserved; + final int offset = 0; + final int limit = decodeSlotOffset; + + decodeNet(traceId, authorization, budgetId, reserved, buffer, offset, limit); + } + } + + private void decodeNet( + long traceId, + long authorization, + long budgetId, + int reserved, + DirectBuffer buffer, + int offset, + int limit) + { + McpServerDecoder previous = null; + int progress = offset; + while (progress <= limit && previous != decoder) + { + previous = decoder; + progress = decoder.decode(this, traceId, authorization, budgetId, reserved, buffer, offset, progress, limit); + } + + if (progress < limit) + { + if (decodeSlot == NO_SLOT) + { + decodeSlot = decodePool.acquire(initialId); + } + + if (decodeSlot == NO_SLOT) + { + cleanupNet(traceId, authorization); + } + else + { + final MutableDirectBuffer slot = decodePool.buffer(decodeSlot); + slot.putBytes(0, buffer, progress, limit - progress); + decodeSlotOffset = limit - progress; + decodeSlotReserved = 0; + decodeParserProgress += progress - offset; + } + } + else + { + cleanupDecodeSlot(); + } + + if (McpState.initialClosing(state) && + decodeSlot == BufferPool.NO_SLOT && + stream != null) + { + state = McpState.closedInitial(state); + stream.doAppEnd(traceId, authorization); + } + } + + private void onLifecycleInitialize( + long traceId, + long authorization) + { + McpLifecycleStream session = new McpLifecycleStream(this); + sessions.put(session.sessionId, session); + + assert this.session == null; + this.session = session; + } + + private void onLifecycleInitialized( + long traceId, + long authorization) + { + assert session != null; + + McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .lifecycle(i -> i + .sessionId(session.sessionId)) + .build(); + session.doAppBegin(traceId, authorization, beginEx); + } + + private int onDecodeInitialize( + long traceId, + long authorization, + DirectBuffer buffer, + int offset, + int limit) + { + // DirectBufferInputStream input = new DirectBufferInputStream(buffer, offset, limit - offset); + // McpInitializeRequestParams params = JsonbBuilder.create().fromJson(input, McpInitializeRequestParams.class); + + onLifecycleInitialize(traceId, authorization); + doEncodeInitialize(traceId, authorization); + + return limit; + } + + private void doEncodeInitialize( + long traceId, + long authorization) + { + doEncodeBeginResponse(traceId, authorization, httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_200)) + .headersItem(h -> h.name(HTTP_HEADER_CONTENT_TYPE).value(CONTENT_TYPE_JSON)) + .headersItem(h -> h.name(HTTP_HEADER_SESSION).value(session.sessionId)) + .build()); + String8FW payload = new String8FW(""" + { + "protocolVersion":"2025-11-25", + "capabilities":{"prompts":{},"resources":{},"tools":{}}, + "serverInfo":{"name":"zilla"} + } + """.replaceAll("\n", "")); + doEncodeData(traceId, authorization, payload.value()); + doEncodeEndResponse(traceId, authorization); + } + + private void onDecodeNotifyInitialized( + long traceId, + long authorization) + { + doEncodeNotifyInitialized(traceId, authorization); + onLifecycleInitialized(traceId, authorization); + } + + private void doEncodeNotifyInitialized( + long traceId, + long authorization) + { + doNetBegin(traceId, authorization, + httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_200)) + .headersItem(h -> h.name(HTTP_HEADER_CONTENT_TYPE).value(CONTENT_TYPE_JSON)) + .headersItem(h -> h.name(HTTP_HEADER_SESSION).value(session.sessionId)) + .build()); + doNetEnd(traceId, authorization); + } + + private void onDecodePing( + long traceId, + long authorization) + { + doEncodeBeginResponse(traceId, authorization, + httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_200)) + .headersItem(h -> h.name(HTTP_HEADER_CONTENT_TYPE).value(CONTENT_TYPE_JSON)) + .headersItem(h -> h.name(HTTP_HEADER_SESSION).value(session.sessionId)) + .build()); + String8FW payload = new String8FW("{}"); + doEncodeData(traceId, authorization, payload.value()); + doEncodeEndResponse(traceId, authorization); + } + + private void onDecodeToolsList( + long traceId, + long authorization) + { + McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .toolsList(t -> t + .sessionId(session.sessionId)) + .build(); + + assert stream == null; + stream = new McpRequestStream(session, decodedId, this); + stream.doAppBegin(traceId, authorization, beginEx); + } + + private void onDecodeToolsCall( + String name, + long traceId, + long authorization) + { + McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .toolsCall(t -> t + .sessionId(session.sessionId) + .name(name)) + .build(); + + assert stream == null; + stream = new McpRequestStream(session, decodedId, this); + stream.doAppBegin(traceId, authorization, beginEx); + } + + private void onDecodePromptsList( + long traceId, + long authorization) + { + McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .promptsList(p -> p + .sessionId(session.sessionId)) + .build(); + + assert stream == null; + stream = new McpRequestStream(session, decodedId, this); + stream.doAppBegin(traceId, authorization, beginEx); + } + + private void onDecodePromptsGet( + String name, + long traceId, + long authorization) + { + McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .promptsGet(p -> p + .sessionId(session.sessionId) + .name(name)) + .build(); + + assert stream == null; + stream = new McpRequestStream(session, decodedId, this); + stream.doAppBegin(traceId, authorization, beginEx); + } + + private void onDecodeResourcesList( + long traceId, + long authorization) + { + McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .resourcesList(r -> r + .sessionId(session.sessionId)) + .build(); + + assert stream == null; + stream = new McpRequestStream(session, decodedId, this); + stream.doAppBegin(traceId, authorization, beginEx); + } + + private void onDecodeResourcesRead( + String uri, + long traceId, + long authorization) + { + McpBeginExFW beginEx = mcpBeginExRW + .wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(mcpTypeId) + .resourcesRead(r -> r + .sessionId(session.sessionId) + .uri(uri)) + .build(); + + assert stream == null; + stream = new McpRequestStream(session, decodedId, this); + stream.doAppBegin(traceId, authorization, beginEx); + } + + private int onDecodeRequestParams( + long traceId, + long authorization, + DirectBuffer buffer, + int offset, + int limit) + { + return stream != null ? stream.doAppData(traceId, authorization, buffer, offset, limit) : offset; + } + + private int onDecodeNotifyCancelled( + long traceId, + long authorization, + DirectBuffer buffer, + int offset, + int limit) + { + DirectBufferInputStream input = new DirectBufferInputStream(buffer, offset, limit - offset); + McpNotifyCanceledParams params = JsonbBuilder.create().fromJson(input, McpNotifyCanceledParams.class); + + assert session != null; + + McpRequestStream request = session.requests.get(params.requestId.toString()); + request.doAppCancel(traceId, authorization); + + doEncodeNotifyCanceled(traceId, authorization); + + return limit; + } + + private void doEncodeNotifyCanceled( + long traceId, + long authorization) + { + doNetBegin(traceId, authorization, + httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_202)) + .headersItem(h -> h.name(HTTP_HEADER_CONTENT_TYPE).value(CONTENT_TYPE_JSON)) + .headersItem(h -> h.name(HTTP_HEADER_SESSION).value(session.sessionId)) + .build()); + doNetEnd(traceId, authorization); + } + + private void onDecodeParseError( + long traceId, + long authorization) + { + doEncodeError(traceId, authorization, + httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_400)) + .build(), + -32700, + "Parse error"); + } + + private void onDecodeInvalidRequest( + long traceId, + long authorization) + { + doEncodeError(traceId, authorization, + httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_400)) + .build(), + -32600, + "Invalid request"); + } + + private void doEncodeBeginResponse( + long traceId, + long authorization, + Flyweight extension) + { + doNetBegin(traceId, authorization, extension); + + final int codecLimit = codecBuffer.putStringWithoutLengthAscii(0, + "{\"jsonrpc\":\"2.0\",\"id\":%s,\"result\":".formatted(decodedId)); + doNetData(traceId, authorization, codecBuffer, 0, codecLimit); + } + + private void doEncodeError( + long traceId, + long authorization, + Flyweight extension, + int code, + String message) + { + doNetBegin(traceId, authorization, extension); + + final int codecLimit = codecBuffer.putStringWithoutLengthAscii(0, + """ + {"jsonrpc":"2.0","id":%s,"error":{"code":%d,"message":"%s"} + """.formatted(decodedId, code, message)); + doNetData(traceId, authorization, codecBuffer, 0, codecLimit); + doNetEnd(traceId, authorization); + } + + private void doEncodeData( + long traceId, + long authorization, + int reserved, + OctetsFW payload) + { + doNetData(traceId, authorization, payload); + } + + private void doEncodeData( + long traceId, + long authorization, + DirectBuffer payload) + { + doNetData(traceId, authorization, payload); + } + + private void doEncodeEndResponse( + long traceId, + long authorization) + { + final int codecLimit = codecBuffer.putStringWithoutLengthAscii(0, "}"); + doNetData(traceId, authorization, codecBuffer, 0, codecLimit); + + state = McpState.closingReply(state); + + if (encodeSlot == BufferPool.NO_SLOT) + { + doNetEnd(traceId, authorization); + } + } + + private void encodeNet( + long traceId, + long authorization, + DirectBuffer buffer, + int offset, + int limit) + { + final int maxLength = limit - offset; + final int minReplyNoAck = (int)(replySeq - replyAck) + encodeSlotOffset; + final int replyWin = replyMax - minReplyNoAck; + final int length = Math.max(Math.min(replyWin - replyPad, maxLength), 0); + + if (length > 0) + { + final int reserved = length + replyPad; + + assert replyBud == 0L; + + doData(net, originId, routedId, replyId, replySeq, replyAck, replyMax, traceId, authorization, + 0x03, replyBud, reserved, buffer, offset, length); + + replySeq += reserved; + + assert replySeq <= replyAck + replyMax : + String.format("%d <= %d + %d", replySeq, replyAck, replyMax); + } + + final int remaining = maxLength - length; + if (remaining > 0) + { + if (encodeSlot == NO_SLOT) + { + encodeSlot = encodePool.acquire(replyId); + } + + if (encodeSlot == NO_SLOT) + { + cleanupNet(traceId, authorization); + } + else + { + final MutableDirectBuffer encodeBuffer = encodePool.buffer(encodeSlot); + encodeBuffer.putBytes(0, buffer, offset + length, remaining); + encodeSlotOffset = remaining; + } + } + else + { + cleanupEncodeSlot(); + + if (McpState.replyClosing(state)) + { + doNetEnd(traceId, authorization); + } + } + + // TODO: needed? + if (stream != null) + { + stream.flushAppWindow(traceId, authorization, 0L, 0, + encodeMax - encodeSlotOffset, encodeMax); + } + } + + private void cleanupDecodeSlot() + { + if (decodeSlot != BufferPool.NO_SLOT) + { + decodePool.release(decodeSlot); + decodeSlot = BufferPool.NO_SLOT; + decodeSlotOffset = 0; + decodeSlotReserved = 0; + } + } + + private void cleanupEncodeSlot() + { + if (encodeSlot != BufferPool.NO_SLOT) + { + encodePool.release(encodeSlot); + encodeSlot = BufferPool.NO_SLOT; + encodeSlotOffset = 0; + encodeSlotTraceId = 0; + } + } + + private void cleanupNet( + long traceId, + long authorization) + { + doNetReset(traceId, authorization); + doNetAbort(traceId, authorization); + } + } + + private final class McpShutdownHandler + { + private final MessageConsumer net; + private final long originId; + private final long routedId; + private final long initialId; + private final long replyId; + + private McpLifecycleStream session; + + private long initialSeq; + private long initialAck; + private int initialMax; + + private long replySeq; + private long replyAck; + private int replyMax; + + private int state; + + private McpShutdownHandler( + MessageConsumer sender, + long originId, + long routedId, + long initialId, + long resolvedId, + McpLifecycleStream session) + { + this.net = sender; + this.originId = originId; + this.routedId = routedId; + this.initialId = initialId; + this.replyId = supplyReplyId.applyAsLong(initialId); + this.session = session; + } + + private void onNetMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onNetBegin(begin); + break; + case DataFW.TYPE_ID: + final DataFW data = dataRO.wrap(buffer, index, index + length); + onNetData(data); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onNetEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onNetAbort(abort); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onNetReset(reset); + break; + default: + break; + } + } + + private void onNetBegin( + BeginFW begin) + { + final long traceId = begin.traceId(); + final long authorization = begin.authorization(); + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final int maximum = begin.maximum(); + + initialSeq = sequence; + initialAck = acknowledge; + initialMax = maximum; + + state = McpState.openedInitial(state); + doNetWindow(traceId, authorization, 0, 0); + + session.doAppEnd(traceId, authorization); + + doNetBegin(traceId, authorization, httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_200)) + .build()); + } + + private void onNetData( + DataFW data) + { + final long sequence = data.sequence(); + final long acknowledge = data.acknowledge(); + final long traceId = data.traceId(); + final long authorization = data.authorization(); + final long budgetId = data.budgetId(); + + assert acknowledge <= sequence; + assert sequence >= initialSeq; + assert acknowledge <= initialAck; + + initialSeq = sequence + data.reserved(); + + assert initialAck <= initialSeq; + + if (initialSeq > initialAck + decodeMax) + { + cleanupNet(traceId, authorization); + } + else + { + initialAck = initialSeq; + doNetWindow(traceId, authorization, budgetId, 0); + } + } + + private void onNetEnd( + EndFW end) + { + final long traceId = end.traceId(); + final long authorization = end.authorization(); + + state = McpState.closingInitial(state); + + doNetEnd(traceId, authorization); + } + + private void onNetAbort( + AbortFW abort) + { + final long traceId = abort.traceId(); + final long authorization = abort.authorization(); + + doNetAbort(traceId, authorization); + } + + private void onNetReset( + ResetFW reset) + { + final long traceId = reset.traceId(); + final long authorization = reset.authorization(); + + doNetReset(traceId, authorization); + } + + private void doNetBegin( + long traceId, + long authorization, + Flyweight extension) + { + if (!McpState.replyOpening(state)) + { + doBegin(net, originId, routedId, replyId, + replySeq, replyAck, replyMax, traceId, authorization, 0, + extension); + + state = McpState.openingReply(state); + } + } + + private void doNetEnd( + long traceId, + long authorization) + { + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + doEnd(net, originId, routedId, replyId, + replySeq, replyAck, replyMax, traceId, authorization); + } + } + + private void doNetAbort( + long traceId, + long authorization) + { + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + doAbort(net, originId, routedId, replyId, + replySeq, replyAck, replyMax, traceId, authorization); + } + } + + private void doNetWindow( + long traceId, + long authorization, + long budgetId, + int padding) + { + doWindow(net, originId, routedId, initialId, + initialSeq, initialAck, decodeMax, traceId, authorization, budgetId, padding); + } + + private void doNetReset( + long traceId, + long authorization) + { + if (!McpState.initialClosed(state)) + { + state = McpState.closedInitial(state); + doReset(net, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, authorization, emptyRO); + } + } + + private void cleanupNet( + long traceId, + long authorization) + { + doNetReset(traceId, authorization); + doNetAbort(traceId, authorization); + } + } + + private final class McpLifecycleStream + { + private final String sessionId; + private final Object2ObjectHashMap requests; + + private final long originId; + private final long routedId; + private final long initialId; + private final long replyId; + private MessageConsumer app; + + private int state; + + private long initialSeq; + private long initialAck; + private int initialMax; + private long replySeq; + private long replyAck; + private int replyMax; + + private McpLifecycleStream( + McpServer server) + { + this.sessionId = supplySessionId.get(); + this.requests = new Object2ObjectHashMap<>(); + this.originId = server.routedId; + this.routedId = server.resolvedId; + this.initialId = supplyInitialId.applyAsLong(server.resolvedId); + this.replyId = supplyReplyId.applyAsLong(initialId); + } + + private void doAppBegin( + long traceId, + long authorization, + Flyweight extension) + { + app = newStream(this::onAppMessage, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, authorization, 0, + extension); + + state = McpState.openingInitial(state); + } + + private void doAppEnd( + long traceId, + long authorization) + { + if (!McpState.initialClosed(state)) + { + state = McpState.closedInitial(state); + doEnd(app, originId, routedId, initialId, + initialSeq, initialAck, initialMax, + traceId, authorization); + } + } + + private void doAppAbort( + long traceId, + long authorization) + { + if (McpState.initialOpened(state) && + !McpState.initialClosed(state)) + { + state = McpState.closedInitial(state); + doAbort(app, originId, routedId, initialId, + initialSeq, initialAck, initialMax, + traceId, authorization); + } + } + + private void doAppWindow( + long traceId, + long authorization, + long budgetId, + int padding) + { + state = McpState.openedReply(state); + doWindow(app, originId, routedId, replyId, + replySeq, replyAck, replyMax, + traceId, authorization, budgetId, padding); + } + + private void doAppReset( + long traceId, + long authorization) + { + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + doReset(app, originId, routedId, replyId, + replySeq, replyAck, replyMax, + traceId, authorization, emptyRO); + } + } + + private void onAppMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onAppBegin(begin); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onAppEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onAppAbort(abort); + break; + case FlushFW.TYPE_ID: + final FlushFW flush = flushRO.wrap(buffer, index, index + length); + onAppFlush(flush); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onAppWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onAppReset(reset); + break; + default: + break; + } + } + + private void onAppBegin( + BeginFW begin) + { + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final long traceId = begin.traceId(); + final long authorization = begin.authorization(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence; + replyAck = acknowledge; + + assert replyAck <= replySeq; + + doAppWindow(traceId, authorization, 0L, 0); + } + + private void onAppFlush( + FlushFW flush) + { + final long sequence = flush.sequence(); + final long acknowledge = flush.acknowledge(); + final long traceId = flush.traceId(); + final long authorization = flush.authorization(); + final int reserved = flush.reserved(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence + reserved; + + assert replyAck <= replySeq; + + if (replySeq > replyAck + decodeMax) + { + cleanupApp(traceId, authorization); + } + } + + private void onAppEnd( + EndFW end) + { + final long traceId = end.traceId(); + final long authorization = end.authorization(); + + doAppEnd(traceId, authorization); + } + + private void onAppAbort( + AbortFW abort) + { + final long traceId = abort.traceId(); + final long authorization = abort.authorization(); + + doAppAbort(traceId, authorization); + } + + private void onAppWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final int maximum = window.maximum(); + + initialSeq = sequence; + initialAck = acknowledge; + initialMax = maximum; + + state = McpState.openedInitial(state); + } + + private void onAppReset( + ResetFW reset) + { + final long traceId = reset.traceId(); + final long authorization = reset.authorization(); + + doAppReset(traceId, authorization); + } + + private void cleanupApp( + long traceId, + long authorization) + { + doAppReset(traceId, authorization); + doAppAbort(traceId, authorization); + } + } + + private final class McpRequestStream + { + private final McpLifecycleStream session; + private final String requestId; + private final McpServer server; + private final long originId; + private final long routedId; + private final long initialId; + private final long replyId; + private MessageConsumer app; + + private int state; + + private long initialSeq; + private long initialAck; + private int initialMax; + private int initialPad; + private long initialBud; + private long replySeq; + private long replyAck; + private int replyMax; + + private McpRequestStream( + McpLifecycleStream session, + String requestId, + McpServer server) + { + this.session = session; + this.requestId = requestId; + this.server = server; + this.originId = server.routedId; + this.routedId = server.resolvedId; + this.initialId = supplyInitialId.applyAsLong(server.resolvedId); + this.replyId = supplyReplyId.applyAsLong(initialId); + } + + private void doAppBegin( + long traceId, + long authorization, + Flyweight extension) + { + app = newStream(this::onAppMessage, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, authorization, 0, + extension); + + state = McpState.openingInitial(state); + + session.requests.put(requestId, this); + } + + private int doAppData( + long traceId, + long authorization, + DirectBuffer buffer, + int offset, + int limit) + { + int initialNoAck = (int)(initialSeq - initialAck); + int length = Math.min(Math.max(initialMax - initialNoAck - initialPad, 0), limit - offset); + + if (length > 0) + { + final int reserved = length + initialPad; + + doData(app, originId, routedId, initialId, initialSeq, initialAck, initialMax, + traceId, authorization, 0x03, initialBud, reserved, buffer, offset, length); + + initialSeq += reserved; + assert initialSeq <= initialAck + initialMax; + } + + return offset + length; + } + + private void doAppFlush( + long traceId, + long authorization, + long budgetId, + int reserved) + { + if (McpState.initialOpened(state)) + { + doFlush(app, originId, routedId, initialId, + initialSeq, initialAck, initialMax, traceId, authorization, + budgetId, reserved); + } + } + + private void doAppEnd( + long traceId, + long authorization) + { + if (!McpState.initialClosed(state)) + { + state = McpState.closedInitial(state); + doEnd(app, originId, routedId, initialId, + initialSeq, initialAck, initialMax, + traceId, authorization); + + if (McpState.replyClosed(state)) + { + session.requests.remove(requestId); + } + } + } + + private void doAppAbort( + long traceId, + long authorization) + { + if (McpState.initialOpened(state) && + !McpState.initialClosed(state)) + { + state = McpState.closedInitial(state); + doAbort(app, originId, routedId, initialId, + initialSeq, initialAck, initialMax, + traceId, authorization); + + if (McpState.replyClosed(state)) + { + session.requests.remove(requestId); + } + } + } + + private void doAppWindow( + long traceId, + long authorization, + long budgetId, + int padding) + { + state = McpState.openedReply(state); + doWindow(app, originId, routedId, replyId, + replySeq, replyAck, replyMax, + traceId, authorization, budgetId, padding); + } + + private void doAppReset( + long traceId, + long authorization) + { + if (!McpState.replyClosed(state)) + { + state = McpState.closedReply(state); + doReset(app, originId, routedId, replyId, + replySeq, replyAck, replyMax, + traceId, authorization, emptyRO); + + if (McpState.initialClosed(state)) + { + session.requests.remove(requestId); + } + } + } + + private void doAppCancel( + long traceId, + long authorization) + { + server.doNetBegin(traceId, authorization, emptyRO); + server.doNetAbort(traceId, authorization); + doAppReset(traceId, authorization); + } + + private void flushAppWindow( + long traceId, + long authorization, + long budgetId, + int padding, + int minReplyNoAck, + int minReplyMax) + { + final long replyAckMax = Math.min(replyAck + minReplyNoAck, replySeq); + if (replyAckMax > replyAck || minReplyMax > replyMax) + { + replyAck = replyAckMax; + assert replyAck <= replySeq; + + replyMax = minReplyMax; + + doAppWindow(traceId, authorization, budgetId, padding); + } + } + + private void onAppMessage( + int msgTypeId, + DirectBuffer buffer, + int index, + int length) + { + switch (msgTypeId) + { + case BeginFW.TYPE_ID: + final BeginFW begin = beginRO.wrap(buffer, index, index + length); + onAppBegin(begin); + break; + case DataFW.TYPE_ID: + final DataFW data = dataRO.wrap(buffer, index, index + length); + onAppData(data); + break; + case EndFW.TYPE_ID: + final EndFW end = endRO.wrap(buffer, index, index + length); + onAppEnd(end); + break; + case AbortFW.TYPE_ID: + final AbortFW abort = abortRO.wrap(buffer, index, index + length); + onAppAbort(abort); + break; + case FlushFW.TYPE_ID: + final FlushFW flush = flushRO.wrap(buffer, index, index + length); + onAppFlush(flush); + break; + case WindowFW.TYPE_ID: + final WindowFW window = windowRO.wrap(buffer, index, index + length); + onAppWindow(window); + break; + case ResetFW.TYPE_ID: + final ResetFW reset = resetRO.wrap(buffer, index, index + length); + onAppReset(reset); + break; + default: + break; + } + } + + private void onAppBegin( + BeginFW begin) + { + final long sequence = begin.sequence(); + final long acknowledge = begin.acknowledge(); + final long traceId = begin.traceId(); + final long authorization = begin.authorization(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence; + replyAck = acknowledge; + + assert replyAck <= replySeq; + + server.doEncodeBeginResponse(traceId, authorization, + httpBeginExRW.wrap(codecBuffer, 0, codecBuffer.capacity()) + .typeId(httpTypeId) + .headersItem(h -> h.name(HTTP_HEADER_STATUS).value(STATUS_200)) + .headersItem(h -> h.name(HTTP_HEADER_CONTENT_TYPE).value(CONTENT_TYPE_JSON)) + .build()); + } + + private void onAppData( + DataFW data) + { + final long sequence = data.sequence(); + final long acknowledge = data.acknowledge(); + final long traceId = data.traceId(); + final long authorization = data.authorization(); + final int reserved = data.reserved(); + final OctetsFW payload = data.payload(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence + reserved; + + assert replyAck <= replySeq; + + if (replySeq > replyAck + decodeMax) + { + cleanupApp(traceId, authorization); + } + else if (payload != null) + { + server.doEncodeData(traceId, authorization, reserved, payload); + } + } + + private void onAppFlush( + FlushFW flush) + { + final long sequence = flush.sequence(); + final long acknowledge = flush.acknowledge(); + final long traceId = flush.traceId(); + final long authorization = flush.authorization(); + final long budgetId = flush.budgetId(); + final int reserved = flush.reserved(); + + assert acknowledge <= sequence; + assert sequence >= replySeq; + assert acknowledge <= replyAck; + + replySeq = sequence + reserved; + + assert replyAck <= replySeq; + + if (replySeq > replyAck + decodeMax) + { + cleanupApp(traceId, authorization); + } + else + { + server.doNetFlush(traceId, authorization, budgetId, reserved); + } + } + + private void onAppEnd( + EndFW end) + { + final long traceId = end.traceId(); + final long authorization = end.authorization(); + + server.doEncodeEndResponse(traceId, authorization); + } + + private void onAppAbort( + AbortFW abort) + { + final long traceId = abort.traceId(); + final long authorization = abort.authorization(); + + server.doNetAbort(traceId, authorization); + } + + private void onAppWindow( + WindowFW window) + { + final long sequence = window.sequence(); + final long acknowledge = window.acknowledge(); + final int maximum = window.maximum(); + final long traceId = window.traceId(); + final long authorization = window.authorization(); + final long budgetId = window.budgetId(); + final int padding = window.padding(); + + assert budgetId == 0L; + + initialSeq = sequence; + initialAck = acknowledge; + initialMax = maximum; + initialBud = budgetId; + initialPad = padding; + + state = McpState.openedInitial(state); + + server.flushNetWindow(traceId, authorization, budgetId); + } + + private void onAppReset( + ResetFW reset) + { + final long traceId = reset.traceId(); + final long authorization = reset.authorization(); + + server.doNetReset(traceId, authorization); + } + + private void cleanupApp( + long traceId, + long authorization) + { + doAppReset(traceId, authorization); + doAppAbort(traceId, authorization); + + server.cleanupNet(traceId, authorization); + } + } + + private MessageConsumer newStream( + MessageConsumer sender, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + final MessageConsumer receiver = + streamFactory.newStream(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof(), sender); + + if (receiver != null) + { + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + } + + return receiver; + } + + private void doBegin( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long affinity, + Flyweight extension) + { + final BeginFW begin = beginRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .affinity(affinity) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(begin.typeId(), begin.buffer(), begin.offset(), begin.sizeof()); + } + + private void doData( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + int flags, + long budgetId, + int reserved, + DirectBuffer payload, + int offset, + int length) + { + final DataFW data = dataRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .flags(flags) + .budgetId(budgetId) + .reserved(reserved) + .payload(payload, offset, length) + .build(); + + receiver.accept(data.typeId(), data.buffer(), data.offset(), data.sizeof()); + } + + private void doEnd( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final EndFW end = endRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(end.typeId(), end.buffer(), end.offset(), end.sizeof()); + } + + private void doAbort( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization) + { + final AbortFW abort = abortRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .build(); + + receiver.accept(abort.typeId(), abort.buffer(), abort.offset(), abort.sizeof()); + } + + private void doFlush( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long budgetId, + int reserved) + { + final FlushFW flush = flushRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .budgetId(budgetId) + .reserved(reserved) + .build(); + + receiver.accept(flush.typeId(), flush.buffer(), flush.offset(), flush.sizeof()); + } + + private void doReset( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + Flyweight extension) + { + final ResetFW reset = resetRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .extension(extension.buffer(), extension.offset(), extension.sizeof()) + .build(); + + receiver.accept(reset.typeId(), reset.buffer(), reset.offset(), reset.sizeof()); + } + + private void doWindow( + MessageConsumer receiver, + long originId, + long routedId, + long streamId, + long sequence, + long acknowledge, + int maximum, + long traceId, + long authorization, + long budgetId, + int padding) + { + final WindowFW window = windowRW.wrap(writeBuffer, 0, writeBuffer.capacity()) + .originId(originId) + .routedId(routedId) + .streamId(streamId) + .sequence(sequence) + .acknowledge(acknowledge) + .maximum(maximum) + .traceId(traceId) + .authorization(authorization) + .budgetId(budgetId) + .padding(padding) + .build(); + + receiver.accept(window.typeId(), window.buffer(), window.offset(), window.sizeof()); + } + + private static int indexOfByte( + DirectBuffer buffer, + int offset, + int limit, + byte value) + { + for (int cursor = offset; cursor < limit; cursor++) + { + if (buffer.getByte(cursor) == value) + { + return cursor; + } + } + + return -1; + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpState.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpState.java new file mode 100644 index 0000000000..ee160e2888 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpState.java @@ -0,0 +1,123 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +public final class McpState +{ + private static final int INITIAL_OPENING = 0x10; + private static final int INITIAL_OPENED = 0x20; + private static final int INITIAL_CLOSING = 0x40; + private static final int INITIAL_CLOSED = 0x80; + private static final int REPLY_OPENED = 0x01; + private static final int REPLY_OPENING = 0x02; + private static final int REPLY_CLOSING = 0x04; + private static final int REPLY_CLOSED = 0x08; + + static int openingInitial( + int state) + { + return state | INITIAL_OPENING; + } + + static int openedInitial( + int state) + { + return state | INITIAL_OPENED; + } + + static boolean initialOpened( + int state) + { + return (state & INITIAL_OPENED) != 0; + } + + static int closingInitial( + int state) + { + return state | INITIAL_CLOSING; + } + + static int closedInitial( + int state) + { + return state | INITIAL_CLOSED; + } + + static boolean initialClosing( + int state) + { + return (state & INITIAL_CLOSING) != 0; + } + + static boolean initialClosed( + int state) + { + return (state & INITIAL_CLOSED) != 0; + } + + static int openingReply( + int state) + { + return state | REPLY_OPENING; + } + + static int openedReply( + int state) + { + return state | REPLY_OPENED; + } + + static boolean replyOpening( + int state) + { + return (state & REPLY_OPENING) != 0; + } + + static boolean replyOpened( + int state) + { + return (state & REPLY_OPENED) != 0; + } + + static int closingReply( + int state) + { + return state | REPLY_CLOSING; + } + + static int closedReply( + int state) + { + return state | REPLY_CLOSED; + } + + static boolean replyClosing( + int state) + { + return (state & REPLY_CLOSING) != 0; + } + + static boolean replyClosed( + int state) + { + return (state & REPLY_CLOSED) != 0; + } + + private McpState() + { + // utility + } +} diff --git a/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpStreamFactory.java b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpStreamFactory.java new file mode 100644 index 0000000000..99251d6717 --- /dev/null +++ b/runtime/binding-mcp/src/main/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpStreamFactory.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import io.aklivity.zilla.runtime.engine.binding.BindingHandler; +import io.aklivity.zilla.runtime.engine.config.BindingConfig; + +public interface McpStreamFactory extends BindingHandler +{ + void attach( + BindingConfig binding); + + void detach( + long bindingId); +} diff --git a/runtime/binding-mcp/src/main/moditect/module-info.java b/runtime/binding-mcp/src/main/moditect/module-info.java new file mode 100644 index 0000000000..389b161dd1 --- /dev/null +++ b/runtime/binding-mcp/src/main/moditect/module-info.java @@ -0,0 +1,30 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +module io.aklivity.zilla.runtime.binding.mcp +{ + requires io.aklivity.zilla.runtime.engine; + + exports io.aklivity.zilla.runtime.binding.mcp.config; + + provides io.aklivity.zilla.runtime.engine.binding.BindingFactorySpi + with io.aklivity.zilla.runtime.binding.mcp.internal.McpBindingFactorySpi; + + provides io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi + with io.aklivity.zilla.runtime.binding.mcp.internal.config.McpOptionsConfigAdapter; + + provides io.aklivity.zilla.runtime.engine.config.ConditionConfigAdapterSpi + with io.aklivity.zilla.runtime.binding.mcp.internal.config.McpConditionConfigAdapter; +} diff --git a/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.binding.BindingFactorySpi b/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.binding.BindingFactorySpi new file mode 100644 index 0000000000..fee8bfc758 --- /dev/null +++ b/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.binding.BindingFactorySpi @@ -0,0 +1 @@ +io.aklivity.zilla.runtime.binding.mcp.internal.McpBindingFactorySpi diff --git a/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.ConditionConfigAdapterSpi b/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.ConditionConfigAdapterSpi new file mode 100644 index 0000000000..f35c3d833c --- /dev/null +++ b/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.ConditionConfigAdapterSpi @@ -0,0 +1 @@ +io.aklivity.zilla.runtime.binding.mcp.internal.config.McpConditionConfigAdapter diff --git a/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi b/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi new file mode 100644 index 0000000000..ceff7e2fa1 --- /dev/null +++ b/runtime/binding-mcp/src/main/resources/META-INF/services/io.aklivity.zilla.runtime.engine.config.OptionsConfigAdapterSpi @@ -0,0 +1 @@ +io.aklivity.zilla.runtime.binding.mcp.internal.config.McpOptionsConfigAdapter diff --git a/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfigurationTest.java b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfigurationTest.java new file mode 100644 index 0000000000..efcab3851c --- /dev/null +++ b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/McpConfigurationTest.java @@ -0,0 +1,32 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfiguration.MCP_SESSION_ID; +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class McpConfigurationTest +{ + public static final String MCP_SESSION_ID_NAME = "zilla.binding.mcp.session.id"; + + @Test + public void shouldVerifyConstants() throws Exception + { + assertEquals(MCP_SESSION_ID.name(), MCP_SESSION_ID_NAME); + } +} diff --git a/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerIT.java b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerIT.java new file mode 100644 index 0000000000..3da684b4e7 --- /dev/null +++ b/runtime/binding-mcp/src/test/java/io/aklivity/zilla/runtime/binding/mcp/internal/stream/McpServerIT.java @@ -0,0 +1,135 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.runtime.binding.mcp.internal.stream; + +import static io.aklivity.zilla.runtime.binding.mcp.internal.McpConfigurationTest.MCP_SESSION_ID_NAME; +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.rules.RuleChain.outerRule; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; + +import io.aklivity.k3po.runtime.junit.annotation.Specification; +import io.aklivity.k3po.runtime.junit.rules.K3poRule; +import io.aklivity.zilla.runtime.engine.test.EngineRule; +import io.aklivity.zilla.runtime.engine.test.annotation.Configuration; + +public class McpServerIT +{ + private final K3poRule k3po = new K3poRule() + .addScriptRoot("net", "io/aklivity/zilla/specs/binding/mcp/streams/network") + .addScriptRoot("app", "io/aklivity/zilla/specs/binding/mcp/streams/application"); + + private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS)); + + private final EngineRule engine = new EngineRule() + .directory("target/zilla-itests") + .countersBufferCapacity(8192) + .configurationRoot("io/aklivity/zilla/specs/binding/mcp/config") + .configure(MCP_SESSION_ID_NAME, "%s::sessionId".formatted(McpServerIT.class.getName())) + .external("app0") + .clean(); + + @Rule + public final TestRule chain = outerRule(engine).around(k3po); //.around(timeout); + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/lifecycle.initialize/client", + "${app}/lifecycle.initialize/server"}) + public void shouldInitializeLifecycle() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/lifecycle.ping/client"}) + public void shouldPingLifecycle() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/lifecycle.shutdown/client", + "${app}/lifecycle.shutdown/server"}) + public void shouldShutdownLifecycle() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/tools.call/client", + "${app}/tools.call/server"}) + public void shouldCallTool() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/tools.list/client", + "${app}/tools.list/server"}) + public void shouldListTools() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/tools.list.canceled/client", + "${app}/tools.list.canceled/server"}) + public void shouldListToolsThenCancel() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/prompts.list/client", + "${app}/prompts.list/server"}) + public void shouldListPrompts() throws Exception + { + k3po.finish(); + } + + @Test + @Configuration("server.yaml") + @Specification({ + "${net}/resources.list/client", + "${app}/resources.list/server"}) + public void shouldListResources() throws Exception + { + k3po.finish(); + } + + public static String sessionId() + { + return "session-1"; + } +} diff --git a/runtime/pom.xml b/runtime/pom.xml index 89be6a6084..aca23facbc 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -29,6 +29,7 @@ binding-http-kafka binding-kafka binding-kafka-grpc + binding-mcp binding-mqtt binding-mqtt-kafka binding-openapi @@ -129,6 +130,11 @@ binding-kafka ${project.version} + + ${project.groupId} + binding-mcp + ${project.version} + ${project.groupId} binding-mqtt diff --git a/specs/binding-mcp.spec/COPYRIGHT b/specs/binding-mcp.spec/COPYRIGHT new file mode 100644 index 0000000000..8b1b7215ef --- /dev/null +++ b/specs/binding-mcp.spec/COPYRIGHT @@ -0,0 +1,13 @@ +Copyright ${copyrightYears} Aklivity Inc. + +Aklivity 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. diff --git a/specs/binding-mcp.spec/LICENSE b/specs/binding-mcp.spec/LICENSE new file mode 100644 index 0000000000..f3cf11c3ad --- /dev/null +++ b/specs/binding-mcp.spec/LICENSE @@ -0,0 +1,114 @@ + Aklivity Community License Agreement + Version 1.0 + +This Aklivity Community License Agreement Version 1.0 (the “Agreement”) sets +forth the terms on which Aklivity, Inc. (“Aklivity”) makes available certain +software made available by Aklivity under this Agreement (the “Software”). BY +INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF THE SOFTWARE, +YOU AGREE TO THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE TO +SUCH TERMS AND CONDITIONS, YOU MUST NOT USE THE SOFTWARE. IF YOU ARE RECEIVING +THE SOFTWARE ON BEHALF OF A LEGAL ENTITY, YOU REPRESENT AND WARRANT THAT YOU +HAVE THE ACTUAL AUTHORITY TO AGREE TO THE TERMS AND CONDITIONS OF THIS +AGREEMENT ON BEHALF OF SUCH ENTITY. “Licensee” means you, an individual, or +the entity on whose behalf you are receiving the Software. + + 1. LICENSE GRANT AND CONDITIONS. + + 1.1 License. Subject to the terms and conditions of this Agreement, + Aklivity hereby grants to Licensee a non-exclusive, royalty-free, + worldwide, non-transferable, non-sublicenseable license during the term + of this Agreement to: (a) use the Software; (b) prepare modifications and + derivative works of the Software; (c) distribute the Software (including + without limitation in source code or object code form); and (d) reproduce + copies of the Software (the “License”). Licensee is not granted the + right to, and Licensee shall not, exercise the License for an Excluded + Purpose. For purposes of this Agreement, “Excluded Purpose” means making + available any software-as-a-service, platform-as-a-service, + infrastructure-as-a-service or other similar online service that competes + with Aklivity products or services that provide the Software. + + 1.2 Conditions. In consideration of the License, Licensee’s distribution + of the Software is subject to the following conditions: + + (a) Licensee must cause any Software modified by Licensee to carry + prominent notices stating that Licensee modified the Software. + + (b) On each Software copy, Licensee shall reproduce and not remove or + alter all Aklivity or third party copyright or other proprietary + notices contained in the Software, and Licensee must provide the + notice below with each copy. + + “This software is made available by Aklivity, Inc., under the + terms of the Aklivity Community License Agreement, Version 1.0 + located at http://www.Aklivity.io/Aklivity-community-license. BY + INSTALLING, DOWNLOADING, ACCESSING, USING OR DISTRIBUTING ANY OF + THE SOFTWARE, YOU AGREE TO THE TERMS OF SUCH LICENSE AGREEMENT.” + + 1.3 Licensee Modifications. Licensee may add its own copyright notices + to modifications made by Licensee and may provide additional or different + license terms and conditions for use, reproduction, or distribution of + Licensee’s modifications. While redistributing the Software or + modifications thereof, Licensee may choose to offer, for a fee or free of + charge, support, warranty, indemnity, or other obligations. Licensee, and + not Aklivity, will be responsible for any such obligations. + + 1.4 No Sublicensing. The License does not include the right to + sublicense the Software, however, each recipient to which Licensee + provides the Software may exercise the Licenses so long as such recipient + agrees to the terms and conditions of this Agreement. + + 2. TERM AND TERMINATION. This Agreement will continue unless and until + earlier terminated as set forth herein. If Licensee breaches any of its + conditions or obligations under this Agreement, this Agreement will + terminate automatically and the License will terminate automatically and + permanently. + + 3. INTELLECTUAL PROPERTY. As between the parties, Aklivity will retain all + right, title, and interest in the Software, and all intellectual property + rights therein. Aklivity hereby reserves all rights not expressly granted + to Licensee in this Agreement. Aklivity hereby reserves all rights in its + trademarks and service marks, and no licenses therein are granted in this + Agreement. + + 4. DISCLAIMER. Aklivity HEREBY DISCLAIMS ANY AND ALL WARRANTIES AND + CONDITIONS, EXPRESS, IMPLIED, STATUTORY, OR OTHERWISE, AND SPECIFICALLY + DISCLAIMS ANY WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR + PURPOSE, WITH RESPECT TO THE SOFTWARE. + + 5. LIMITATION OF LIABILITY. Aklivity WILL NOT BE LIABLE FOR ANY DAMAGES OF + ANY KIND, INCLUDING BUT NOT LIMITED TO, LOST PROFITS OR ANY CONSEQUENTIAL, + SPECIAL, INCIDENTAL, INDIRECT, OR DIRECT DAMAGES, HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, ARISING OUT OF THIS AGREEMENT. THE FOREGOING SHALL + APPLY TO THE EXTENT PERMITTED BY APPLICABLE LAW. + + 6.GENERAL. + + 6.1 Governing Law. This Agreement will be governed by and interpreted in + accordance with the laws of the state of California, without reference to + its conflict of laws principles. If Licensee is located within the + United States, all disputes arising out of this Agreement are subject to + the exclusive jurisdiction of courts located in Santa Clara County, + California. USA. If Licensee is located outside of the United States, + any dispute, controversy or claim arising out of or relating to this + Agreement will be referred to and finally determined by arbitration in + accordance with the JAMS International Arbitration Rules. The tribunal + will consist of one arbitrator. The place of arbitration will be Palo + Alto, California. The language to be used in the arbitral proceedings + will be English. Judgment upon the award rendered by the arbitrator may + be entered in any court having jurisdiction thereof. + + 6.2 Assignment. Licensee is not authorized to assign its rights under + this Agreement to any third party. Aklivity may freely assign its rights + under this Agreement to any third party. + + 6.3 Other. This Agreement is the entire agreement between the parties + regarding the subject matter hereof. No amendment or modification of + this Agreement will be valid or binding upon the parties unless made in + writing and signed by the duly authorized representatives of both + parties. In the event that any provision, including without limitation + any condition, of this Agreement is held to be unenforceable, this + Agreement and all licenses and rights granted hereunder will immediately + terminate. Waiver by Aklivity of a breach of any provision of this + Agreement or the failure by Aklivity to exercise any right hereunder + will not be construed as a waiver of any subsequent breach of that right + or as a waiver of any other right. \ No newline at end of file diff --git a/specs/binding-mcp.spec/NOTICE b/specs/binding-mcp.spec/NOTICE new file mode 100644 index 0000000000..65641b77d8 --- /dev/null +++ b/specs/binding-mcp.spec/NOTICE @@ -0,0 +1,20 @@ +Licensed under the Aklivity Community License (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + https://www.aklivity.io/aklivity-community-license/ + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +This project includes: + agrona under The Apache License, Version 2.0 + ICU4J under Unicode/ICU License + Jakarta JSON Processing API under Eclipse Public License 2.0 or GNU General Public License, version 2 with the GNU Classpath Exception + org.leadpony.justify under The Apache Software License, Version 2.0 + zilla::specs::binding-http.spec under The Apache Software License, Version 2.0 + zilla::specs::binding-proxy.spec under The Apache Software License, Version 2.0 + zilla::specs::engine.spec under The Apache Software License, Version 2.0 + diff --git a/specs/binding-mcp.spec/NOTICE.template b/specs/binding-mcp.spec/NOTICE.template new file mode 100644 index 0000000000..209ca12f74 --- /dev/null +++ b/specs/binding-mcp.spec/NOTICE.template @@ -0,0 +1,13 @@ +Licensed under the Aklivity Community License (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + https://www.aklivity.io/aklivity-community-license/ + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +This project includes: +#GENERATED_NOTICES# diff --git a/specs/binding-mcp.spec/pom.xml b/specs/binding-mcp.spec/pom.xml new file mode 100644 index 0000000000..836f07fd7d --- /dev/null +++ b/specs/binding-mcp.spec/pom.xml @@ -0,0 +1,164 @@ + + + + 4.0.0 + + io.aklivity.zilla + specs + develop-SNAPSHOT + ../pom.xml + + + binding-mcp.spec + zilla::specs::binding-mcp.spec + + + + Aklivity Community License Agreement + https://www.aklivity.io/aklivity-community-license/ + repo + + + + + 0.96 + 0 + + + + + io.aklivity.k3po + lang + provided + + + ${project.groupId} + engine.spec + ${project.version} + + + ${project.groupId} + binding-http.spec + ${project.version} + + + junit + junit + test + + + io.aklivity.k3po + control-junit + test + + + org.hamcrest + hamcrest-library + test + + + + + + + src/main/resources + + + src/main/scripts + + + + + + org.jasig.maven + maven-notice-plugin + + + ${project.groupId} + flyweight-maven-plugin + ${project.version} + + core http mcp + io.aklivity.zilla.specs.binding.mcp.internal.types + + + + + validate + generate + + + + + + com.mycila + license-maven-plugin + + + maven-checkstyle-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.moditect + moditect-maven-plugin + + + io.aklivity.k3po + k3po-maven-plugin + + + ${project.groupId} + engine + ${project.version} + test-jar + + + ${project.groupId} + engine + ${project.version} + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + org.jacoco + jacoco-maven-plugin + + + io/aklivity/zilla/specs/binding/mcp/internal/types/**/*.class + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + ${jacoco.coverage.ratio} + + + CLASS + MISSEDCOUNT + ${jacoco.missed.count} + + + + + + + + + diff --git a/specs/binding-mcp.spec/src/main/java/io/aklivity/zilla/specs/binding/mcp/internal/McpFunctions.java b/specs/binding-mcp.spec/src/main/java/io/aklivity/zilla/specs/binding/mcp/internal/McpFunctions.java new file mode 100644 index 0000000000..ac5f6c68a9 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/java/io/aklivity/zilla/specs/binding/mcp/internal/McpFunctions.java @@ -0,0 +1,756 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.specs.binding.mcp.internal; + +import java.nio.ByteBuffer; +import java.util.function.Predicate; + +import org.agrona.DirectBuffer; +import org.agrona.MutableDirectBuffer; +import org.agrona.concurrent.UnsafeBuffer; + +import io.aklivity.k3po.runtime.lang.el.BytesMatcher; +import io.aklivity.k3po.runtime.lang.el.Function; +import io.aklivity.k3po.runtime.lang.el.spi.FunctionMapperSpi; +import io.aklivity.zilla.specs.binding.mcp.internal.types.String16FW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpAbortExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpBeginExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpLifecycleBeginExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpPromptsGetBeginExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpPromptsListBeginExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpResourcesListBeginExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpResourcesReadBeginExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpToolsCallBeginExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpToolsListBeginExFW; + +public final class McpFunctions +{ + @Function + public static McpBeginExBuilder beginEx() + { + return new McpBeginExBuilder(); + } + + @Function + public static McpBeginExMatcherBuilder matchBeginEx() + { + return new McpBeginExMatcherBuilder(); + } + + public static final class McpBeginExBuilder + { + private final MutableDirectBuffer writeBuffer = new UnsafeBuffer(new byte[1024]); + private final McpBeginExFW.Builder beginExRW = new McpBeginExFW.Builder(); + + private McpBeginExBuilder() + { + beginExRW.wrap(writeBuffer, 0, writeBuffer.capacity()); + } + + public McpBeginExBuilder typeId( + int typeId) + { + beginExRW.typeId(typeId); + return this; + } + + public McpLifecycleBeginExBuilder lifecycle() + { + return new McpLifecycleBeginExBuilder(); + } + + public McpToolsListBeginExBuilder toolsList() + { + return new McpToolsListBeginExBuilder(); + } + + public McpToolsCallBeginExBuilder toolsCall() + { + return new McpToolsCallBeginExBuilder(); + } + + public McpPromptsListBeginExBuilder promptsList() + { + return new McpPromptsListBeginExBuilder(); + } + + public McpPromptsGetBeginExBuilder promptsGet() + { + return new McpPromptsGetBeginExBuilder(); + } + + public McpResourcesListBeginExBuilder resourcesList() + { + return new McpResourcesListBeginExBuilder(); + } + + public McpResourcesReadBeginExBuilder resourcesRead() + { + return new McpResourcesReadBeginExBuilder(); + } + + public byte[] build() + { + final byte[] array = new byte[beginExRW.limit()]; + writeBuffer.getBytes(0, array); + return array; + } + + public final class McpLifecycleBeginExBuilder + { + private String sessionId; + + public McpLifecycleBeginExBuilder sessionId( + String sessionId) + { + this.sessionId = sessionId; + return this; + } + + public McpBeginExBuilder build() + { + beginExRW.lifecycle(b -> b.sessionId(sessionId)); + return McpBeginExBuilder.this; + } + } + + public final class McpToolsListBeginExBuilder + { + private String sessionId; + + public McpToolsListBeginExBuilder sessionId( + String sessionId) + { + this.sessionId = sessionId; + return this; + } + + public McpBeginExBuilder build() + { + beginExRW.toolsList(b -> b.sessionId(sessionId)); + return McpBeginExBuilder.this; + } + } + + public final class McpToolsCallBeginExBuilder + { + private String sessionId; + private String name; + + public McpToolsCallBeginExBuilder sessionId( + String sessionId) + { + this.sessionId = sessionId; + return this; + } + + public McpToolsCallBeginExBuilder name( + String name) + { + this.name = name; + return this; + } + + public McpBeginExBuilder build() + { + beginExRW.toolsCall(b -> b.sessionId(sessionId).name(name)); + return McpBeginExBuilder.this; + } + } + + public final class McpPromptsListBeginExBuilder + { + private String sessionId; + + public McpPromptsListBeginExBuilder sessionId( + String sessionId) + { + this.sessionId = sessionId; + return this; + } + + public McpBeginExBuilder build() + { + beginExRW.promptsList(b -> b.sessionId(sessionId)); + return McpBeginExBuilder.this; + } + } + + public final class McpPromptsGetBeginExBuilder + { + private String sessionId; + private String name; + + public McpPromptsGetBeginExBuilder sessionId( + String sessionId) + { + this.sessionId = sessionId; + return this; + } + + public McpPromptsGetBeginExBuilder name( + String name) + { + this.name = name; + return this; + } + + public McpBeginExBuilder build() + { + beginExRW.promptsGet(b -> b.sessionId(sessionId).name(name)); + return McpBeginExBuilder.this; + } + } + + public final class McpResourcesListBeginExBuilder + { + private String sessionId; + + public McpResourcesListBeginExBuilder sessionId( + String sessionId) + { + this.sessionId = sessionId; + return this; + } + + public McpBeginExBuilder build() + { + beginExRW.resourcesList(b -> b.sessionId(sessionId)); + return McpBeginExBuilder.this; + } + } + + public final class McpResourcesReadBeginExBuilder + { + private String sessionId; + private String uri; + + public McpResourcesReadBeginExBuilder sessionId( + String sessionId) + { + this.sessionId = sessionId; + return this; + } + + public McpResourcesReadBeginExBuilder uri( + String uri) + { + this.uri = uri; + return this; + } + + public McpBeginExBuilder build() + { + beginExRW.resourcesRead(b -> b.sessionId(sessionId).uri(uri)); + return McpBeginExBuilder.this; + } + } + } + + public static final class McpBeginExMatcherBuilder + { + private final DirectBuffer bufferRO = new UnsafeBuffer(); + private final McpBeginExFW beginExRO = new McpBeginExFW(); + + private Integer typeId; + private Integer kind; + private Predicate caseMatcher; + + public McpBeginExMatcherBuilder typeId( + int typeId) + { + this.typeId = typeId; + return this; + } + + public McpLifecycleBeginExMatcherBuilder lifecycle() + { + this.kind = McpBeginExFW.KIND_LIFECYCLE; + final McpLifecycleBeginExMatcherBuilder matcher = new McpLifecycleBeginExMatcherBuilder(); + this.caseMatcher = matcher::match; + return matcher; + } + + public McpToolsListBeginExMatcherBuilder toolsList() + { + this.kind = McpBeginExFW.KIND_TOOLS_LIST; + final McpToolsListBeginExMatcherBuilder matcher = new McpToolsListBeginExMatcherBuilder(); + this.caseMatcher = matcher::match; + return matcher; + } + + public McpToolsCallBeginExMatcherBuilder toolsCall() + { + this.kind = McpBeginExFW.KIND_TOOLS_CALL; + final McpToolsCallBeginExMatcherBuilder matcher = new McpToolsCallBeginExMatcherBuilder(); + this.caseMatcher = matcher::match; + return matcher; + } + + public McpPromptsListBeginExMatcherBuilder promptsList() + { + this.kind = McpBeginExFW.KIND_PROMPTS_LIST; + final McpPromptsListBeginExMatcherBuilder matcher = new McpPromptsListBeginExMatcherBuilder(); + this.caseMatcher = matcher::match; + return matcher; + } + + public McpPromptsGetBeginExMatcherBuilder promptsGet() + { + this.kind = McpBeginExFW.KIND_PROMPTS_GET; + final McpPromptsGetBeginExMatcherBuilder matcher = new McpPromptsGetBeginExMatcherBuilder(); + this.caseMatcher = matcher::match; + return matcher; + } + + public McpResourcesListBeginExMatcherBuilder resourcesList() + { + this.kind = McpBeginExFW.KIND_RESOURCES_LIST; + final McpResourcesListBeginExMatcherBuilder matcher = new McpResourcesListBeginExMatcherBuilder(); + this.caseMatcher = matcher::match; + return matcher; + } + + public McpResourcesReadBeginExMatcherBuilder resourcesRead() + { + this.kind = McpBeginExFW.KIND_RESOURCES_READ; + final McpResourcesReadBeginExMatcherBuilder matcher = new McpResourcesReadBeginExMatcherBuilder(); + this.caseMatcher = matcher::match; + return matcher; + } + + public BytesMatcher build() + { + return typeId != null || kind != null ? this::match : buf -> null; + } + + private McpBeginExFW match( + ByteBuffer byteBuf) throws Exception + { + if (!byteBuf.hasRemaining()) + { + return null; + } + + bufferRO.wrap(byteBuf); + final McpBeginExFW beginEx = beginExRO.tryWrap(bufferRO, byteBuf.position(), byteBuf.capacity()); + + if (beginEx != null && + matchTypeId(beginEx) && + matchKind(beginEx) && + matchCase(beginEx)) + { + byteBuf.position(byteBuf.position() + beginEx.sizeof()); + return beginEx; + } + + throw new Exception(beginEx != null ? beginEx.toString() : "null"); + } + + private boolean matchTypeId( + McpBeginExFW beginEx) + { + return typeId == null || typeId == beginEx.typeId(); + } + + private boolean matchKind( + McpBeginExFW beginEx) + { + return kind == null || kind == beginEx.kind(); + } + + private boolean matchCase( + McpBeginExFW beginEx) + { + return caseMatcher == null || caseMatcher.test(beginEx); + } + + public final class McpLifecycleBeginExMatcherBuilder + { + private String16FW sessionId; + + public McpLifecycleBeginExMatcherBuilder sessionId( + String sessionId) + { + this.sessionId = new String16FW(sessionId); + return this; + } + + public McpBeginExMatcherBuilder build() + { + return McpBeginExMatcherBuilder.this; + } + + private boolean match( + McpBeginExFW beginEx) + { + final McpLifecycleBeginExFW lifecycle = beginEx.lifecycle(); + return matchSessionId(lifecycle); + } + + private boolean matchSessionId( + McpLifecycleBeginExFW lifecycle) + { + return sessionId == null || sessionId.equals(lifecycle.sessionId()); + } + } + + public final class McpToolsListBeginExMatcherBuilder + { + private String16FW sessionId; + + public McpToolsListBeginExMatcherBuilder sessionId( + String sessionId) + { + this.sessionId = new String16FW(sessionId); + return this; + } + + public McpBeginExMatcherBuilder build() + { + return McpBeginExMatcherBuilder.this; + } + + private boolean match( + McpBeginExFW beginEx) + { + return matchSessionId(beginEx.toolsList()); + } + + private boolean matchSessionId( + McpToolsListBeginExFW toolsList) + { + return sessionId == null || sessionId.equals(toolsList.sessionId()); + } + } + + public final class McpToolsCallBeginExMatcherBuilder + { + private String16FW sessionId; + private String16FW name; + + public McpToolsCallBeginExMatcherBuilder sessionId( + String sessionId) + { + this.sessionId = new String16FW(sessionId); + return this; + } + + public McpToolsCallBeginExMatcherBuilder name( + String name) + { + this.name = new String16FW(name); + return this; + } + + public McpBeginExMatcherBuilder build() + { + return McpBeginExMatcherBuilder.this; + } + + private boolean match( + McpBeginExFW beginEx) + { + final McpToolsCallBeginExFW toolsCall = beginEx.toolsCall(); + return matchSessionId(toolsCall) && matchName(toolsCall); + } + + private boolean matchSessionId( + McpToolsCallBeginExFW toolsCall) + { + return sessionId == null || sessionId.equals(toolsCall.sessionId()); + } + + private boolean matchName( + McpToolsCallBeginExFW toolsCall) + { + return name == null || name.equals(toolsCall.name()); + } + } + + public final class McpPromptsListBeginExMatcherBuilder + { + private String16FW sessionId; + + public McpPromptsListBeginExMatcherBuilder sessionId( + String sessionId) + { + this.sessionId = new String16FW(sessionId); + return this; + } + + public McpBeginExMatcherBuilder build() + { + return McpBeginExMatcherBuilder.this; + } + + private boolean match( + McpBeginExFW beginEx) + { + return matchSessionId(beginEx.promptsList()); + } + + private boolean matchSessionId( + McpPromptsListBeginExFW promptsList) + { + return sessionId == null || sessionId.equals(promptsList.sessionId()); + } + } + + public final class McpPromptsGetBeginExMatcherBuilder + { + private String16FW sessionId; + private String16FW name; + + public McpPromptsGetBeginExMatcherBuilder sessionId( + String sessionId) + { + this.sessionId = new String16FW(sessionId); + return this; + } + + public McpPromptsGetBeginExMatcherBuilder name( + String name) + { + this.name = new String16FW(name); + return this; + } + + public McpBeginExMatcherBuilder build() + { + return McpBeginExMatcherBuilder.this; + } + + private boolean match( + McpBeginExFW beginEx) + { + final McpPromptsGetBeginExFW promptsGet = beginEx.promptsGet(); + return matchSessionId(promptsGet) && matchName(promptsGet); + } + + private boolean matchSessionId( + McpPromptsGetBeginExFW promptsGet) + { + return sessionId == null || sessionId.equals(promptsGet.sessionId()); + } + + private boolean matchName( + McpPromptsGetBeginExFW promptsGet) + { + return name == null || name.equals(promptsGet.name()); + } + } + + public final class McpResourcesListBeginExMatcherBuilder + { + private String16FW sessionId; + + public McpResourcesListBeginExMatcherBuilder sessionId( + String sessionId) + { + this.sessionId = new String16FW(sessionId); + return this; + } + + public McpBeginExMatcherBuilder build() + { + return McpBeginExMatcherBuilder.this; + } + + private boolean match( + McpBeginExFW beginEx) + { + return matchSessionId(beginEx.resourcesList()); + } + + private boolean matchSessionId( + McpResourcesListBeginExFW resourcesList) + { + return sessionId == null || sessionId.equals(resourcesList.sessionId()); + } + } + + public final class McpResourcesReadBeginExMatcherBuilder + { + private String16FW sessionId; + private String16FW uri; + + public McpResourcesReadBeginExMatcherBuilder sessionId( + String sessionId) + { + this.sessionId = new String16FW(sessionId); + return this; + } + + public McpResourcesReadBeginExMatcherBuilder uri( + String uri) + { + this.uri = new String16FW(uri); + return this; + } + + public McpBeginExMatcherBuilder build() + { + return McpBeginExMatcherBuilder.this; + } + + private boolean match( + McpBeginExFW beginEx) + { + final McpResourcesReadBeginExFW resourcesRead = beginEx.resourcesRead(); + return matchSessionId(resourcesRead) && matchUri(resourcesRead); + } + + private boolean matchSessionId( + McpResourcesReadBeginExFW resourcesRead) + { + return sessionId == null || sessionId.equals(resourcesRead.sessionId()); + } + + private boolean matchUri( + McpResourcesReadBeginExFW resourcesRead) + { + return uri == null || uri.equals(resourcesRead.uri()); + } + } + } + + @Function + public static McpAbortExBuilder abortEx() + { + return new McpAbortExBuilder(); + } + + @Function + public static McpAbortExMatcherBuilder matchAbortEx() + { + return new McpAbortExMatcherBuilder(); + } + + public static final class McpAbortExBuilder + { + private final MutableDirectBuffer writeBuffer = new UnsafeBuffer(new byte[256]); + private final McpAbortExFW.Builder abortExRW = new McpAbortExFW.Builder(); + + private McpAbortExBuilder() + { + abortExRW.wrap(writeBuffer, 0, writeBuffer.capacity()); + } + + public McpAbortExBuilder typeId( + int typeId) + { + abortExRW.typeId(typeId); + return this; + } + + public McpAbortExBuilder reason( + String reason) + { + abortExRW.reason(reason); + return this; + } + + public byte[] build() + { + final McpAbortExFW abortEx = abortExRW.build(); + final byte[] array = new byte[abortEx.sizeof()]; + writeBuffer.getBytes(abortEx.offset(), array); + return array; + } + } + + public static final class McpAbortExMatcherBuilder + { + private final DirectBuffer bufferRO = new UnsafeBuffer(); + private final McpAbortExFW abortExRO = new McpAbortExFW(); + + private Integer typeId; + private String16FW reason; + + public McpAbortExMatcherBuilder typeId( + int typeId) + { + this.typeId = typeId; + return this; + } + + public McpAbortExMatcherBuilder reason( + String reason) + { + this.reason = new String16FW(reason); + return this; + } + + public BytesMatcher build() + { + return typeId != null || reason != null ? this::match : buf -> null; + } + + private McpAbortExFW match( + ByteBuffer byteBuf) throws Exception + { + if (!byteBuf.hasRemaining()) + { + return null; + } + + bufferRO.wrap(byteBuf); + final McpAbortExFW abortEx = abortExRO.tryWrap(bufferRO, byteBuf.position(), byteBuf.capacity()); + + if (abortEx != null && + matchTypeId(abortEx) && + matchReason(abortEx)) + { + byteBuf.position(byteBuf.position() + abortEx.sizeof()); + return abortEx; + } + + throw new Exception(abortEx != null ? abortEx.toString() : "null"); + } + + private boolean matchTypeId( + McpAbortExFW abortEx) + { + return typeId == null || typeId == abortEx.typeId(); + } + + private boolean matchReason( + McpAbortExFW abortEx) + { + return reason == null || reason.equals(abortEx.reason()); + } + } + + public static class Mapper extends FunctionMapperSpi.Reflective + { + public Mapper() + { + super(McpFunctions.class); + } + + @Override + public String getPrefixName() + { + return "mcp"; + } + } + + private McpFunctions() + { + // utility + } +} diff --git a/specs/binding-mcp.spec/src/main/moditect/module-info.java b/specs/binding-mcp.spec/src/main/moditect/module-info.java new file mode 100644 index 0000000000..daa51311a9 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/moditect/module-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +open module io.aklivity.zilla.specs.binding.mcp +{ + requires transitive io.aklivity.zilla.specs.engine; + requires transitive io.aklivity.zilla.specs.binding.http; +} diff --git a/specs/binding-mcp.spec/src/main/resources/META-INF/services/io.aklivity.k3po.runtime.lang.el.spi.FunctionMapperSpi b/specs/binding-mcp.spec/src/main/resources/META-INF/services/io.aklivity.k3po.runtime.lang.el.spi.FunctionMapperSpi new file mode 100644 index 0000000000..e3535475cc --- /dev/null +++ b/specs/binding-mcp.spec/src/main/resources/META-INF/services/io.aklivity.k3po.runtime.lang.el.spi.FunctionMapperSpi @@ -0,0 +1 @@ +io.aklivity.zilla.specs.binding.mcp.internal.McpFunctions$Mapper diff --git a/specs/binding-mcp.spec/src/main/resources/META-INF/zilla/mcp.idl b/specs/binding-mcp.spec/src/main/resources/META-INF/zilla/mcp.idl new file mode 100644 index 0000000000..e975e7f63e --- /dev/null +++ b/specs/binding-mcp.spec/src/main/resources/META-INF/zilla/mcp.idl @@ -0,0 +1,74 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +scope mcp +{ + scope stream + { + union McpBeginEx switch (uint8) extends core::stream::Extension + { + case 0: mcp::stream::McpLifecycleBeginEx lifecycle; + case 1: mcp::stream::McpToolsListBeginEx toolsList; + case 2: mcp::stream::McpToolsCallBeginEx toolsCall; + case 3: mcp::stream::McpPromptsListBeginEx promptsList; + case 4: mcp::stream::McpPromptsGetBeginEx promptsGet; + case 5: mcp::stream::McpResourcesListBeginEx resourcesList; + case 6: mcp::stream::McpResourcesReadBeginEx resourcesRead; + } + + struct McpLifecycleBeginEx + { + string16 sessionId; + } + + struct McpToolsListBeginEx + { + string16 sessionId; + } + + struct McpToolsCallBeginEx + { + string16 sessionId; + string16 name; + } + + struct McpPromptsListBeginEx + { + string16 sessionId; + } + + struct McpPromptsGetBeginEx + { + string16 sessionId; + string16 name; + } + + struct McpResourcesListBeginEx + { + string16 sessionId; + } + + struct McpResourcesReadBeginEx + { + string16 sessionId; + string16 uri; + } + + struct McpAbortEx extends core::stream::Extension + { + string16 reason = null; + } + } +} diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/server.yaml b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/server.yaml new file mode 100644 index 0000000000..6bd1483386 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/config/server.yaml @@ -0,0 +1,24 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +--- +name: test +bindings: + net0: + type: mcp + kind: server + routes: + - exit: app0 diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json new file mode 100644 index 0000000000..9fdcf97613 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json @@ -0,0 +1,100 @@ +[ + { + "op": "add", + "path": "/$defs/binding/properties/type/enum/-", + "value": "mcp" + }, + { + "op": "add", + "path": "/$defs/binding/allOf/-", + "value": + { + "if": + { + "properties": + { + "type": + { + "const": "mcp" + } + } + }, + "then": + { + "properties": + { + "type": + { + "const": "mcp" + }, + "kind": + { + "enum": [ "server" ] + }, + "options": + { + "properties": + { + "prompts": + { + "title": "Local Prompts", + "type": "array", + "items": + { + "type": "object", + "properties": + { + "name": + { + "title": "Prompt Name", + "type": "string" + }, + "description": + { + "title": "Prompt Description", + "type": "string" + } + }, + "required": [ "name" ], + "additionalProperties": false + } + } + }, + "additionalProperties": false + }, + "routes": + { + "title": "Routes", + "type": "array", + "items": + { + "type": "object", + "properties": + { + "when": + { + "title": "When", + "type": "array", + "items": + { + "type": "object", + "properties": + { + "kind": + { + "title": "Stream Kind", + "type": "string", + "enum": [ "server", "proxy", "client", "callback" ] + } + }, + "additionalProperties": false + } + } + } + } + } + } + } + } + } +] diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.initialize/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.initialize/client.rpt new file mode 100644 index 0000000000..5825d98186 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.initialize/client.rpt @@ -0,0 +1,35 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.initialize/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.initialize/server.rpt new file mode 100644 index 0000000000..881fae7102 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.initialize/server.rpt @@ -0,0 +1,38 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} +write flush diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.shutdown/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.shutdown/client.rpt new file mode 100644 index 0000000000..0ce47240cd --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.shutdown/client.rpt @@ -0,0 +1,39 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +write close + +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.shutdown/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.shutdown/server.rpt new file mode 100644 index 0000000000..a48ae2bcbf --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/lifecycle.shutdown/server.rpt @@ -0,0 +1,42 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} +write flush + +read closed + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/prompts.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/prompts.list/client.rpt new file mode 100644 index 0000000000..f2aeb5f343 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/prompts.list/client.rpt @@ -0,0 +1,56 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("session-1") + .build() + .build()} + +connected + +write close + +read '{"prompts":[]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/prompts.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/prompts.list/server.rpt new file mode 100644 index 0000000000..8e309d51d2 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/prompts.list/server.rpt @@ -0,0 +1,56 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .promptsList() + .sessionId("session-1") + .build() + .build()} + +connected + +read closed + +write '{"prompts":[]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/resources.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/resources.list/client.rpt new file mode 100644 index 0000000000..5d46fb76d0 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/resources.list/client.rpt @@ -0,0 +1,56 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("session-1") + .build() + .build()} + +connected + +write close + +read '{"resources":[]}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/resources.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/resources.list/server.rpt new file mode 100644 index 0000000000..116a12b2a5 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/resources.list/server.rpt @@ -0,0 +1,56 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .resourcesList() + .sessionId("session-1") + .build() + .build()} + +connected + +read closed + +write '{"resources":[]}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.call/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.call/client.rpt new file mode 100644 index 0000000000..c12ab33cf2 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.call/client.rpt @@ -0,0 +1,73 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsCall() + .sessionId("session-1") + .name("get_weather") + .build() + .build()} + +connected + +write '{' + '"name":"get_weather",' + '"arguments":' + '{' + '"location": "New York"' + '}' + '}' +write close + +read '{' + '"content":' + '[' + '{' + '"type": "text",' + '"text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"' + '}' + '],' + '"isError": false' + '}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.call/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.call/server.rpt new file mode 100644 index 0000000000..a4a2f808b7 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.call/server.rpt @@ -0,0 +1,73 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsCall() + .sessionId("session-1") + .name("get_weather") + .build() + .build()} + +connected + +read '{' + '"name":"get_weather",' + '"arguments":' + '{' + '"location": "New York"' + '}' + '}' +read closed + +write '{' + '"content":' + '[' + '{' + '"type": "text",' + '"text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"' + '}' + '],' + '"isError": false' + '}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list.canceled/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list.canceled/client.rpt new file mode 100644 index 0000000000..757117f79a --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list.canceled/client.rpt @@ -0,0 +1,55 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("session-1") + .build() + .build()} + +connected + +write close + +read abort diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list.canceled/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list.canceled/server.rpt new file mode 100644 index 0000000000..fb96c2dd81 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list.canceled/server.rpt @@ -0,0 +1,55 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("session-1") + .build() + .build()} + +connected + +read closed + +write flush + +write aborted diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list/client.rpt new file mode 100644 index 0000000000..3f3bc0c93c --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list/client.rpt @@ -0,0 +1,84 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("session-1") + .build() + .build()} + +connected + +write close + +read '{"tools":' + '[' + '{' + '"name": "get_weather",' + '"title": "Weather Information Provider",' + '"description": "Get current weather information for a location",' + '"inputSchema": {' + '"type": "object",' + '"properties": {' + '"location": {' + '"type": "string",' + '"description": "City name or zip code"' + '}' + '},' + '"required": ["location"]' + '},' + '"icons": [' + '{' + '"src": "https://example.com/weather-icon.png",' + '"mimeType": "image/png",' + '"sizes": ["48x48"]' + '}' + '],' + '"execution": {' + '"taskSupport": "optional"' + '}' + '}' + ']' + '}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list/server.rpt new file mode 100644 index 0000000000..8c8fbbf67e --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/application/tools.list/server.rpt @@ -0,0 +1,84 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/app0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} + +connected + +write zilla:begin.ext ${mcp:beginEx() + .typeId(zilla:id("mcp")) + .lifecycle() + .sessionId("session-1") + .build() + .build()} +write flush + +accepted + +read zilla:begin.ext ${mcp:matchBeginEx() + .typeId(zilla:id("mcp")) + .toolsList() + .sessionId("session-1") + .build() + .build()} + +connected + +read closed + +write '{"tools":' + '[' + '{' + '"name": "get_weather",' + '"title": "Weather Information Provider",' + '"description": "Get current weather information for a location",' + '"inputSchema": {' + '"type": "object",' + '"properties": {' + '"location": {' + '"type": "string",' + '"description": "City name or zip code"' + '}' + '},' + '"required": ["location"]' + '},' + '"icons": [' + '{' + '"src": "https://example.com/weather-icon.png",' + '"mimeType": "image/png",' + '"sizes": ["48x48"]' + '}' + '],' + '"execution": {' + '"taskSupport": "optional"' + '}' + '}' + ']' + '}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.initialize/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.initialize/client.rpt new file mode 100644 index 0000000000..0f13f69aa1 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.initialize/client.rpt @@ -0,0 +1,68 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} + +read '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +read closed +read notify LIFECYCLE_INITIALIZING + +connect await LIFECYCLE_INITIALIZING + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/initialized"}' +write close + +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.initialize/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.initialize/server.rpt new file mode 100644 index 0000000000..d0ec4728cd --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.initialize/server.rpt @@ -0,0 +1,71 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/initialized"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.ping/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.ping/client.rpt new file mode 100644 index 0000000000..5a6be30bf0 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.ping/client.rpt @@ -0,0 +1,99 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} + +read '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +read closed +read notify LIFECYCLE_INITIALIZING + +connect await LIFECYCLE_INITIALIZING + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/initialized"}' +write close + +read closed +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":3,"method":"ping"}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} + +read '{"jsonrpc":"2.0","id":3,"result":{}}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.ping/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.ping/server.rpt new file mode 100644 index 0000000000..b5eb3dc2b4 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.ping/server.rpt @@ -0,0 +1,100 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/initialized"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":3,"method":"ping"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("content-length", "36") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":3,"result":{}}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.shutdown/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.shutdown/client.rpt new file mode 100644 index 0000000000..174e7fe402 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.shutdown/client.rpt @@ -0,0 +1,95 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} + +read '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +read closed +read notify LIFECYCLE_INITIALIZING + +connect await LIFECYCLE_INITIALIZING + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/initialized"}' +write close + +read closed +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "DELETE") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .build()} + +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.shutdown/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.shutdown/server.rpt new file mode 100644 index 0000000000..204fa80d09 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/lifecycle.shutdown/server.rpt @@ -0,0 +1,93 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/initialized"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "DELETE") + .header(":path", "/mcp") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .build()} +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/prompts.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/prompts.list/client.rpt new file mode 100644 index 0000000000..77d9f7c2ca --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/prompts.list/client.rpt @@ -0,0 +1,99 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} + +read '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +read closed +read notify LIFECYCLE_INITIALIZING + +connect await LIFECYCLE_INITIALIZING + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/initialized"}' +write close + +read closed +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":5,"method":"prompts/list"}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} + +read '{"jsonrpc":"2.0","id":5,"result":{"prompts":[]}}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/prompts.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/prompts.list/server.rpt new file mode 100644 index 0000000000..2e9d19b58f --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/prompts.list/server.rpt @@ -0,0 +1,99 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/initialized"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":5,"method":"prompts/list"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":5,"result":{"prompts":[]}}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/resources.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/resources.list/client.rpt new file mode 100644 index 0000000000..e51dd01708 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/resources.list/client.rpt @@ -0,0 +1,99 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} + +read '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +read closed +read notify LIFECYCLE_INITIALIZING + +connect await LIFECYCLE_INITIALIZING + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/initialized"}' +write close + +read closed +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":6,"method":"resources/list"}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} + +read '{"jsonrpc":"2.0","id":6,"result":{"resources":[]}}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/resources.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/resources.list/server.rpt new file mode 100644 index 0000000000..afda3e6cd9 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/resources.list/server.rpt @@ -0,0 +1,99 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/initialized"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":6,"method":"resources/list"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":6,"result":{"resources":[]}}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.call/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.call/client.rpt new file mode 100644 index 0000000000..de18c3d9e5 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.call/client.rpt @@ -0,0 +1,100 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} + +read '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +read closed +read notify LIFECYCLE_INITIALIZING + +connect await LIFECYCLE_INITIALIZING + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/initialized"}' +write close + +read closed +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_weather","arguments":{"location": "New York"}}}' + +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} + +read '{"jsonrpc":"2.0","id":2,"result":{"content":[{"type": "text","text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"}],"isError": false}}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.call/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.call/server.rpt new file mode 100644 index 0000000000..c42b880474 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.call/server.rpt @@ -0,0 +1,99 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/initialized"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_weather","arguments":{"location": "New York"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":2,"result":{"content":[{"type": "text","text": "Current weather in New York:\nTemperature: 72°F\nConditions: Partly cloudy"}],"isError": false}}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list.canceled/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list.canceled/client.rpt new file mode 100644 index 0000000000..22fe4887f2 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list.canceled/client.rpt @@ -0,0 +1,128 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} + +read '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +read closed +read notify LIFECYCLE_INITIALIZING + +connect await LIFECYCLE_INITIALIZING + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/initialized"}' +write close + +read closed +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} + +read '{"jsonrpc":"2.0","id":2,"result":' +read notify RESPONSE_IN_PROGRESS +read aborted + +connect await RESPONSE_IN_PROGRESS + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":2,"reason":"User cancelled"}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} + +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list.canceled/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list.canceled/server.rpt new file mode 100644 index 0000000000..7e10b68119 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list.canceled/server.rpt @@ -0,0 +1,124 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/initialized"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":2,"result":' +write await REQUEST_CANCELED +write abort + + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/cancelled","params":{"requestId":2,"reason":"User cancelled"}}' +read closed +read notify REQUEST_CANCELED + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list/client.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list/client.rpt new file mode 100644 index 0000000000..a966fe7970 --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list/client.rpt @@ -0,0 +1,129 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +connect "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} + +read '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +read closed +read notify LIFECYCLE_INITIALIZING + +connect await LIFECYCLE_INITIALIZING + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","method":"notifications/initialized"}' +write close + +read closed +read notify LIFECYCLE_INITIALIZED + +connect await LIFECYCLE_INITIALIZED + "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":scheme", "http") + .header(":authority", "localhost:8080") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +write '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' +write close + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} + +read '{"jsonrpc":"2.0","id":2,"result":' + '{"tools":' + '[' + '{' + '"name": "get_weather",' + '"title": "Weather Information Provider",' + '"description": "Get current weather information for a location",' + '"inputSchema": {' + '"type": "object",' + '"properties": {' + '"location": {' + '"type": "string",' + '"description": "City name or zip code"' + '}' + '},' + '"required": ["location"]' + '},' + '"icons": [' + '{' + '"src": "https://example.com/weather-icon.png",' + '"mimeType": "image/png",' + '"sizes": ["48x48"]' + '}' + '],' + '"execution": {' + '"taskSupport": "optional"' + '}' + '}' + ']' + '}' + '}' +read closed diff --git a/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list/server.rpt b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list/server.rpt new file mode 100644 index 0000000000..cae4e591be --- /dev/null +++ b/specs/binding-mcp.spec/src/main/scripts/io/aklivity/zilla/specs/binding/mcp/streams/network/tools.list/server.rpt @@ -0,0 +1,129 @@ +# +# Copyright 2021-2024 Aklivity Inc. +# +# Aklivity 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. +# + +accept "zilla://streams/net0" + option zilla:window 8192 + option zilla:transmission "half-duplex" + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}}}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .header("mcp-session-id", "session-1") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","capabilities":{"prompts":{},"resources":{},"tools":{}},"serverInfo":{"name":"zilla"}}}' +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","method":"notifications/initialized"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "202") + .build()} +write flush + +write close + +accepted + +read zilla:begin.ext ${http:matchBeginEx() + .typeId(zilla:id("http")) + .header(":method", "POST") + .header(":path", "/mcp") + .header("content-type", "application/json") + .header("mcp-protocol-version", "2025-11-25") + .header("mcp-session-id", "session-1") + .build()} + +connected + +read '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' +read closed + +write zilla:begin.ext ${http:beginEx() + .typeId(zilla:id("http")) + .header(":status", "200") + .header("content-type", "application/json") + .build()} +write flush + +write '{"jsonrpc":"2.0","id":2,"result":' + '{"tools":' + '[' + '{' + '"name": "get_weather",' + '"title": "Weather Information Provider",' + '"description": "Get current weather information for a location",' + '"inputSchema": {' + '"type": "object",' + '"properties": {' + '"location": {' + '"type": "string",' + '"description": "City name or zip code"' + '}' + '},' + '"required": ["location"]' + '},' + '"icons": [' + '{' + '"src": "https://example.com/weather-icon.png",' + '"mimeType": "image/png",' + '"sizes": ["48x48"]' + '}' + '],' + '"execution": {' + '"taskSupport": "optional"' + '}' + '}' + ']' + '}' + '}' +write flush + +write close diff --git a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/config/SchemaTest.java b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/config/SchemaTest.java new file mode 100644 index 0000000000..56402fcd53 --- /dev/null +++ b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/config/SchemaTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.specs.binding.mcp.config; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.nullValue; + +import jakarta.json.JsonObject; + +import org.junit.Rule; +import org.junit.Test; + +import io.aklivity.zilla.specs.engine.config.ConfigSchemaRule; + +public class SchemaTest +{ + @Rule + public final ConfigSchemaRule schema = new ConfigSchemaRule() + .schemaPatch("io/aklivity/zilla/specs/binding/mcp/schema/mcp.schema.patch.json") + .configurationRoot("io/aklivity/zilla/specs/binding/mcp/config"); + + @Test + public void shouldValidateServer() + { + JsonObject config = schema.validate("server.yaml"); + + assertThat(config, not(nullValue())); + } +} diff --git a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/internal/McpFunctionsTest.java b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/internal/McpFunctionsTest.java new file mode 100644 index 0000000000..e9de41490f --- /dev/null +++ b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/internal/McpFunctionsTest.java @@ -0,0 +1,454 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.specs.binding.mcp.internal; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; + +import org.agrona.concurrent.UnsafeBuffer; +import org.junit.Test; + +import io.aklivity.k3po.runtime.lang.el.BytesMatcher; +import io.aklivity.zilla.specs.binding.mcp.internal.types.String16FW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpAbortExFW; +import io.aklivity.zilla.specs.binding.mcp.internal.types.stream.McpBeginExFW; + +public class McpFunctionsTest +{ + @Test + public void shouldGetPrefixName() + { + assertNotNull(new McpFunctions.Mapper().getPrefixName()); + } + + @Test + public void shouldGenerateLifecycleBeginEx() + { + byte[] bytes = McpFunctions.beginEx() + .typeId(0) + .lifecycle() + .sessionId("session-1") + .build() + .build(); + + assertNotNull(bytes); + } + + @Test + public void shouldMatchLifecycleBeginEx() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .lifecycle() + .sessionId("session-1") + .build() + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpBeginExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .lifecycle(b -> b.sessionId("session-1")) + .build(); + + assertNotNull(matcher.match(byteBuf)); + } + + @Test + public void shouldGenerateToolsListBeginEx() + { + byte[] bytes = McpFunctions.beginEx() + .typeId(0) + .toolsList() + .sessionId("session-1") + .build() + .build(); + + assertNotNull(bytes); + } + + @Test + public void shouldMatchToolsListBeginEx() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .toolsList() + .sessionId("session-1") + .build() + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpBeginExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .toolsList(b -> b.sessionId("session-1")) + .build(); + + assertNotNull(matcher.match(byteBuf)); + } + + @Test + public void shouldGenerateToolsCallBeginEx() + { + byte[] bytes = McpFunctions.beginEx() + .typeId(0) + .toolsCall() + .sessionId("session-1") + .name("my-tool") + .build() + .build(); + + assertNotNull(bytes); + } + + @Test + public void shouldMatchToolsCallBeginEx() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .toolsCall() + .sessionId("session-1") + .name("my-tool") + .build() + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpBeginExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .toolsCall(b -> b + .sessionId("session-1") + .name("my-tool")) + .build(); + + assertNotNull(matcher.match(byteBuf)); + } + + @Test + public void shouldGeneratePromptsListBeginEx() + { + byte[] bytes = McpFunctions.beginEx() + .typeId(0) + .promptsList() + .sessionId("session-1") + .build() + .build(); + + assertNotNull(bytes); + } + + @Test + public void shouldMatchPromptsListBeginEx() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .promptsList() + .sessionId("session-1") + .build() + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpBeginExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .promptsList(b -> b + .sessionId("session-1")) + .build(); + + assertNotNull(matcher.match(byteBuf)); + } + + @Test + public void shouldGeneratePromptsGetBeginEx() + { + byte[] bytes = McpFunctions.beginEx() + .typeId(0) + .promptsGet() + .sessionId("session-1") + .name("my-prompt") + .build() + .build(); + + assertNotNull(bytes); + } + + @Test + public void shouldMatchPromptsGetBeginEx() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .promptsGet() + .sessionId("session-1") + .name("my-prompt") + .build() + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpBeginExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .promptsGet(b -> b + .sessionId("session-1") + .name("my-prompt")) + .build(); + + assertNotNull(matcher.match(byteBuf)); + } + + @Test + public void shouldGenerateResourcesListBeginEx() + { + byte[] bytes = McpFunctions.beginEx() + .typeId(0) + .resourcesList() + .sessionId("session-1") + .build() + .build(); + + assertNotNull(bytes); + } + + @Test + public void shouldMatchResourcesBeginEx() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .resourcesList() + .sessionId("session-1") + .build() + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpBeginExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .resourcesList(b -> b + .sessionId("session-1")) + .build(); + + assertNotNull(matcher.match(byteBuf)); + } + + @Test + public void shouldGenerateResourcesReadBeginEx() + { + byte[] bytes = McpFunctions.beginEx() + .typeId(0) + .resourcesRead() + .sessionId("session-1") + .uri("file:///data/resource.txt") + .build() + .build(); + + assertNotNull(bytes); + } + + @Test + public void shouldMatchResourcesReadBeginEx() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .resourcesRead() + .sessionId("session-1") + .uri("file:///data/resource.txt") + .build() + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpBeginExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .resourcesRead(b -> b + .sessionId("session-1") + .uri("file:///data/resource.txt")) + .build(); + + assertNotNull(matcher.match(byteBuf)); + } + + @Test(expected = Exception.class) + public void shouldFailWhenCaseMismatch() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .lifecycle() + .build() + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpBeginExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .toolsList(b -> b.sessionId("session-1")) + .build(); + + matcher.match(byteBuf); + } + + @Test + public void shouldReturnNullWhenBufferIsEmpty() throws Exception + { + BytesMatcher matcher = McpFunctions.matchBeginEx() + .typeId(0) + .build(); + + assertNull(matcher.match(ByteBuffer.allocate(0))); + } + + @Test + public void shouldGenerateAbortEx() + { + byte[] bytes = McpFunctions.abortEx() + .typeId(0) + .reason("connection reset") + .build(); + + assertNotNull(bytes); + } + + @Test + public void shouldMatchAbortEx() throws Exception + { + BytesMatcher matcher = McpFunctions.matchAbortEx() + .typeId(0) + .reason("connection reset") + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpAbortExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .reason("connection reset") + .build(); + + assertNotNull(matcher.match(byteBuf)); + } + + @Test + public void shouldReturnNullWhenAbortExMatcherIsEmpty() throws Exception + { + BytesMatcher matcher = McpFunctions.matchAbortEx() + .build(); + + assertNull(matcher.match(ByteBuffer.allocate(0))); + } + + @Test(expected = Exception.class) + public void shouldFailWhenAbortExReasonMismatch() throws Exception + { + BytesMatcher matcher = McpFunctions.matchAbortEx() + .typeId(0) + .reason("expected reason") + .build(); + + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpAbortExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .reason("actual reason") + .build(); + + matcher.match(byteBuf); + } + + @Test + public void shouldExerciseAbortExBuilderOverloads() throws Exception + { + // reason(String16FW) overload + ByteBuffer byteBuf = ByteBuffer.allocate(256); + + new McpAbortExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .reason(new String16FW("string-fw-reason")) + .build(); + + assertNotNull(McpFunctions.matchAbortEx() + .typeId(0) + .reason("string-fw-reason") + .build() + .match(byteBuf)); + + // reason(Consumer) overload + byteBuf.clear(); + + new McpAbortExFW.Builder() + .wrap(new UnsafeBuffer(byteBuf), 0, byteBuf.capacity()) + .typeId(0) + .reason(b -> b.set("consumer-reason", StandardCharsets.UTF_8)) + .build(); + + assertNotNull(McpFunctions.matchAbortEx() + .typeId(0) + .reason("consumer-reason") + .build() + .match(byteBuf)); + } + + @Test + public void shouldWrapAndReadAbortEx() + { + ByteBuffer byteBuf = ByteBuffer.allocate(256); + final UnsafeBuffer buffer = new UnsafeBuffer(byteBuf); + + new McpAbortExFW.Builder() + .wrap(buffer, 0, buffer.capacity()) + .typeId(0) + .reason("wrap-test") + .build(); + + final McpAbortExFW abortEx = new McpAbortExFW(); + abortEx.wrap(buffer, 0, buffer.capacity()); + + assertNotNull(abortEx.reason()); + } + + @Test + public void shouldCopyAbortEx() + { + ByteBuffer srcBuf = ByteBuffer.allocate(256); + final UnsafeBuffer srcBuffer = new UnsafeBuffer(srcBuf); + + final McpAbortExFW source = new McpAbortExFW.Builder() + .wrap(srcBuffer, 0, srcBuffer.capacity()) + .typeId(0) + .reason("copy-test") + .build(); + + ByteBuffer dstBuf = ByteBuffer.allocate(256); + final UnsafeBuffer dstBuffer = new UnsafeBuffer(dstBuf); + + final McpAbortExFW copy = new McpAbortExFW.Builder() + .wrap(dstBuffer, 0, dstBuffer.capacity()) + .set(source) + .build(); + + assertNotNull(copy.reason()); + } +} diff --git a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/application/ApplicationIT.java b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/application/ApplicationIT.java new file mode 100644 index 0000000000..c8655ae535 --- /dev/null +++ b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/application/ApplicationIT.java @@ -0,0 +1,102 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.specs.binding.mcp.streams.application; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.rules.RuleChain.outerRule; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; + +import io.aklivity.k3po.runtime.junit.annotation.Specification; +import io.aklivity.k3po.runtime.junit.rules.K3poRule; + +public class ApplicationIT +{ + private final K3poRule k3po = new K3poRule() + .addScriptRoot("app", "io/aklivity/zilla/specs/binding/mcp/streams/application"); + + private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS)); + + @Rule + public final TestRule chain = outerRule(k3po).around(timeout); + + @Test + @Specification({ + "${app}/lifecycle.initialize/client", + "${app}/lifecycle.initialize/server"}) + public void shouldInitializeLifecycle() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/lifecycle.shutdown/client", + "${app}/lifecycle.shutdown/server"}) + public void shouldShutdownLifecycle() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/tools.call/client", + "${app}/tools.call/server"}) + public void shouldCallTool() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/tools.list/client", + "${app}/tools.list/server"}) + public void shouldListTools() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/tools.list.canceled/client", + "${app}/tools.list.canceled/server"}) + public void shouldListToolsThenAbort() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/prompts.list/client", + "${app}/prompts.list/server"}) + public void shouldListPrompts() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${app}/resources.list/client", + "${app}/resources.list/server"}) + public void shouldListResources() throws Exception + { + k3po.finish(); + } +} diff --git a/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/network/NetworkIT.java b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/network/NetworkIT.java new file mode 100644 index 0000000000..a5b1b5dca2 --- /dev/null +++ b/specs/binding-mcp.spec/src/test/java/io/aklivity/zilla/specs/binding/mcp/streams/network/NetworkIT.java @@ -0,0 +1,111 @@ +/* + * Copyright 2021-2024 Aklivity Inc. + * + * Aklivity 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. + */ +package io.aklivity.zilla.specs.binding.mcp.streams.network; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.junit.rules.RuleChain.outerRule; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.DisableOnDebug; +import org.junit.rules.TestRule; +import org.junit.rules.Timeout; + +import io.aklivity.k3po.runtime.junit.annotation.Specification; +import io.aklivity.k3po.runtime.junit.rules.K3poRule; + +public class NetworkIT +{ + private final K3poRule k3po = new K3poRule() + .addScriptRoot("net", "io/aklivity/zilla/specs/binding/mcp/streams/network"); + + private final TestRule timeout = new DisableOnDebug(new Timeout(10, SECONDS)); + + @Rule + public final TestRule chain = outerRule(k3po).around(timeout); + + @Test + @Specification({ + "${net}/lifecycle.initialize/client", + "${net}/lifecycle.initialize/server"}) + public void shouldInitializeLifecycle() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${net}/lifecycle.shutdown/client", + "${net}/lifecycle.shutdown/server"}) + public void shouldShutdownLifecycle() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${net}/lifecycle.ping/client", + "${net}/lifecycle.ping/server"}) + public void shouldPingLifecycle() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${net}/tools.call/client", + "${net}/tools.call/server"}) + public void shouldCallTool() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${net}/tools.list/client", + "${net}/tools.list/server"}) + public void shouldListTools() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${net}/tools.list.canceled/client", + "${net}/tools.list.canceled/server"}) + public void shouldListToolsThenCancel() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${net}/prompts.list/client", + "${net}/prompts.list/server"}) + public void shouldListPrompts() throws Exception + { + k3po.finish(); + } + + @Test + @Specification({ + "${net}/resources.list/client", + "${net}/resources.list/server"}) + public void shouldListResources() throws Exception + { + k3po.finish(); + } +} diff --git a/specs/catalog-schema-registry.spec/NOTICE b/specs/catalog-schema-registry.spec/NOTICE new file mode 100644 index 0000000000..fa9b8adebc --- /dev/null +++ b/specs/catalog-schema-registry.spec/NOTICE @@ -0,0 +1,22 @@ +Licensed under the Aklivity Community License (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + https://www.aklivity.io/aklivity-community-license/ + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +This project includes: + agrona under The Apache License, Version 2.0 + ANTLR 4 Runtime under BSD-3-Clause + ICU4J under Unicode/ICU License + Jakarta JSON Processing API under Eclipse Public License 2.0 or GNU General Public License, version 2 with the GNU Classpath Exception + Java Unified Expression Language API under The Apache Software License, Version 2.0 + Java Unified Expression Language Implementation under The Apache Software License, Version 2.0 + k3po::runtime::lang under The Apache Software License, Version 2.0 + org.leadpony.justify under The Apache Software License, Version 2.0 + zilla::specs::engine.spec under The Apache Software License, Version 2.0 + diff --git a/specs/catalog-schema-registry.spec/NOTICE.template b/specs/catalog-schema-registry.spec/NOTICE.template new file mode 100644 index 0000000000..209ca12f74 --- /dev/null +++ b/specs/catalog-schema-registry.spec/NOTICE.template @@ -0,0 +1,13 @@ +Licensed under the Aklivity Community License (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at + + https://www.aklivity.io/aklivity-community-license/ + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. + +This project includes: +#GENERATED_NOTICES# diff --git a/specs/pom.xml b/specs/pom.xml index ab6a9239f6..52cb9e77e0 100644 --- a/specs/pom.xml +++ b/specs/pom.xml @@ -27,6 +27,7 @@ binding-tls.spec binding-http.spec binding-grpc.spec + binding-mcp.spec binding-mqtt.spec binding-openapi.spec binding-openapi-asyncapi.spec @@ -122,6 +123,11 @@ binding-http-filesystem.spec ${project.version} + + ${project.groupId} + binding-mcp.spec + ${project.version} + ${project.groupId} binding-mqtt.spec