diff --git a/spack_repo/eic/packages/eic_mcp/package.py b/spack_repo/eic/packages/eic_mcp/package.py new file mode 100644 index 00000000..a32b942a --- /dev/null +++ b/spack_repo/eic/packages/eic_mcp/package.py @@ -0,0 +1,44 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.build_systems.generic import Package + +from spack.package import * + + +class EicMcp(Package): + """Service manager for the EIC MCP servers inside eic-shell. + + Starts/stops the installed MCP servers as local SSE services (bridging + stdio servers via supergateway) and writes client configs, so an outer + LLM client (opencode, Claude Code, Copilot, ...) can drive them over + 127.0.0.1. Serves what is installed; it does not build or fetch servers.""" + + homepage = "https://github.com/eic/eic-mcp" + git = "https://github.com/eic/eic-mcp.git" + + maintainers("wdconinc") + + tags = ["eic"] + + license("MIT", checked_by="aprozo") + + version("main", branch="main") + # Service-management-only launcher (eic/eic-mcp#1); retag once released. + version("0.1.0", commit="4a5c610458dc1df94439aecce1b749f2ba11ad60") + + # Bridge for stdio-only servers. + depends_on("supergateway", type="run") + + # The servers eic-mcp serves. They are runtime, not build, dependencies: + # eic-mcp only launches their installed console entry points. + depends_on("uproot-mcp-server", type="run") + depends_on("rucio-eic-mcp-server", type="run") + depends_on("xrootd-mcp-server", type="run") + depends_on("zenodo-mcp-server", type="run") + + def install(self, spec, prefix): + mkdirp(prefix.bin) + install(join_path("bin", "eic-mcp"), join_path(prefix.bin, "eic-mcp")) + set_executable(join_path(prefix.bin, "eic-mcp")) diff --git a/spack_repo/eic/packages/opencode/package.py b/spack_repo/eic/packages/opencode/package.py new file mode 100644 index 00000000..88686788 --- /dev/null +++ b/spack_repo/eic/packages/opencode/package.py @@ -0,0 +1,56 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +import platform + +from spack_repo.builtin.build_systems.generic import Package + +from spack.package import * + +# Upstream ships a self-contained (Bun single-file) binary per platform. +# The npm wrapper package (opencode-ai) resolves its platform binary in a +# postinstall script via optionalDependencies, which does not work in an +# offline spack install phase — so install the GitHub release binary instead +# (same pattern as the builtin cudnn-style per-platform version tables). +_versions = { + "1.18.3": { + "Linux-x86_64": "60f27b2679f00a511b6539f97e02448afaf58d9c66e2448285ea0c517ca84583", + "Linux-aarch64": "da0a631174eba380b2a1d51f9d364fa3812da433e72743c72471d4b5da59c69d", + }, +} + +_arch_map = {"x86_64": "x64", "aarch64": "arm64"} + + +class Opencode(Package): + """opencode — an open-source AI coding agent for the terminal. + + Runs as a TUI or headless CLI against MCP servers and LLM providers, so + agentic workflows work on machines without a graphical client (farm + nodes, CI reproducers, ...).""" + + homepage = "https://opencode.ai" + url = "https://github.com/anomalyco/opencode/releases/download/v1.18.3/opencode-linux-x64.tar.gz" + + maintainers("wdconinc") + + tags = ["eic"] + + license("MIT", checked_by="aprozo") + + for _ver, _shas in _versions.items(): + _key = f"{platform.system()}-{platform.machine()}" + if _key in _shas: + _arch = _arch_map[platform.machine()] + version( + _ver, + sha256=_shas[_key], + url=f"https://github.com/anomalyco/opencode/releases/download/v{_ver}/opencode-linux-{_arch}.tar.gz", + ) + + def install(self, spec, prefix): + # The tarball contains the single `opencode` binary at its root. + mkdirp(prefix.bin) + install("opencode", join_path(prefix.bin, "opencode")) + set_executable(join_path(prefix.bin, "opencode")) diff --git a/spack_repo/eic/packages/py_httpx_sse/package.py b/spack_repo/eic/packages/py_httpx_sse/package.py new file mode 100644 index 00000000..65ee437a --- /dev/null +++ b/spack_repo/eic/packages/py_httpx_sse/package.py @@ -0,0 +1,29 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.build_systems.python import PythonPackage + +from spack.package import * + + +class PyHttpxSse(PythonPackage): + """Consume Server-Sent Events (SSE) with HTTPX.""" + + homepage = "https://github.com/florimondmanca/httpx-sse" + pypi = "httpx-sse/httpx-sse-0.4.0.tar.gz" + + maintainers("wdconinc") + + tags = ["eic"] + + license("MIT", checked_by="aprozo") + + version("0.4.0", sha256="1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721") + + depends_on("python@3.8:", type=("build", "run")) + depends_on("py-setuptools", type="build") + depends_on("py-setuptools-scm", type="build") + depends_on("py-wheel", type="build") + + depends_on("py-httpx", type=("build", "run")) diff --git a/spack_repo/eic/packages/py_mcp/package.py b/spack_repo/eic/packages/py_mcp/package.py new file mode 100644 index 00000000..97d3a474 --- /dev/null +++ b/spack_repo/eic/packages/py_mcp/package.py @@ -0,0 +1,47 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.build_systems.python import PythonPackage + +from spack.package import * + + +class PyMcp(PythonPackage): + """The official Python SDK for the Model Context Protocol (MCP).""" + + homepage = "https://github.com/modelcontextprotocol/python-sdk" + pypi = "mcp/mcp-1.28.1.tar.gz" + + maintainers("wdconinc") + + tags = ["eic"] + + license("MIT", checked_by="aprozo") + + version("1.28.1", sha256="d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683") + + variant("cli", default=True, description="Command-line tooling (mcp CLI)") + + depends_on("python@3.10:", type=("build", "run")) + depends_on("py-hatchling", type="build") + + depends_on("py-anyio@4.5:", type=("build", "run")) + depends_on("py-httpx@0.27.1:1", type=("build", "run")) + depends_on("py-httpx-sse@0.4:", type=("build", "run")) + depends_on("py-jsonschema@4.20:", type=("build", "run")) + depends_on("py-pydantic@2.11:2", type=("build", "run")) + depends_on("py-pydantic-settings@2.5.2:", type=("build", "run")) + # pyjwt[crypto]: the crypto extra just adds cryptography + depends_on("py-pyjwt@2.10.1:", type=("build", "run")) + depends_on("py-cryptography", type=("build", "run")) + depends_on("py-python-multipart@0.0.9:", type=("build", "run")) + depends_on("py-sse-starlette@1.6.1:", type=("build", "run")) + depends_on("py-starlette@0.27:", type=("build", "run")) + depends_on("py-typing-extensions@4.9:", type=("build", "run")) + depends_on("py-typing-inspection@0.4.1:", type=("build", "run")) + depends_on("py-uvicorn@0.31.1:", type=("build", "run")) + + with when("+cli"): + depends_on("py-python-dotenv@1:", type=("build", "run")) + depends_on("py-typer@0.16:", type=("build", "run")) diff --git a/spack_repo/eic/packages/py_pyjwt/package.py b/spack_repo/eic/packages/py_pyjwt/package.py new file mode 100644 index 00000000..09232b11 --- /dev/null +++ b/spack_repo/eic/packages/py_pyjwt/package.py @@ -0,0 +1,21 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.packages.py_pyjwt.package import PyPyjwt as BuiltinPyPyjwt + +from spack.package import * + + +class PyPyjwt(BuiltinPyPyjwt): + __doc__ = BuiltinPyPyjwt.__doc__ + + # py-mcp needs pyjwt>=2.10.1; builtin stops at 2.4.0. Newer sdists use the + # PEP 625 lowercase filename, so the version carries its own url. + version( + "2.10.1", + sha256="3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", + url="https://files.pythonhosted.org/packages/source/p/pyjwt/pyjwt-2.10.1.tar.gz", + ) + + depends_on("python@3.9:", when="@2.10:", type=("build", "run")) diff --git a/spack_repo/eic/packages/py_restrictedpython/package.py b/spack_repo/eic/packages/py_restrictedpython/package.py new file mode 100644 index 00000000..6e09d78d --- /dev/null +++ b/spack_repo/eic/packages/py_restrictedpython/package.py @@ -0,0 +1,26 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.build_systems.python import PythonPackage + +from spack.package import * + + +class PyRestrictedpython(PythonPackage): + """A restricted execution environment for Python to run untrusted code.""" + + homepage = "https://github.com/zopefoundation/RestrictedPython" + pypi = "RestrictedPython/restrictedpython-8.1.tar.gz" + + maintainers("wdconinc") + + tags = ["eic"] + + license("ZPL-2.1", checked_by="aprozo") + + version("8.1", sha256="4a69304aceacf6bee74bdf153c728221d4e3109b39acbfe00b3494927080d898") + + depends_on("python@3.9:", type=("build", "run")) + depends_on("py-setuptools@78.1.1:80", type="build") + depends_on("py-wheel", type="build") diff --git a/spack_repo/eic/packages/py_sse_starlette/package.py b/spack_repo/eic/packages/py_sse_starlette/package.py new file mode 100644 index 00000000..9e3efec9 --- /dev/null +++ b/spack_repo/eic/packages/py_sse_starlette/package.py @@ -0,0 +1,29 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.build_systems.python import PythonPackage + +from spack.package import * + + +class PySseStarlette(PythonPackage): + """SSE plugin for Starlette / FastAPI, serving Server-Sent Events.""" + + homepage = "https://github.com/sysid/sse-starlette" + pypi = "sse-starlette/sse_starlette-2.1.3.tar.gz" + + maintainers("wdconinc") + + tags = ["eic"] + + license("BSD-3-Clause", checked_by="aprozo") + + version("2.1.3", sha256="9cd27eb35319e1414e3d2558ee7414487f9529ce3b3cf9b21434fd110e017169") + + depends_on("python@3.8:", type=("build", "run")) + depends_on("py-pdm-backend", type="build") + + depends_on("py-anyio", type=("build", "run")) + depends_on("py-starlette", type=("build", "run")) + depends_on("py-uvicorn", type=("build", "run")) diff --git a/spack_repo/eic/packages/rucio_eic_mcp_server/package.py b/spack_repo/eic/packages/rucio_eic_mcp_server/package.py new file mode 100644 index 00000000..a4155ff9 --- /dev/null +++ b/spack_repo/eic/packages/rucio_eic_mcp_server/package.py @@ -0,0 +1,34 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.build_systems.python import PythonPackage + +from spack.package import * + + +class RucioEicMcpServer(PythonPackage): + """An MCP server for querying Rucio data-management for the EIC. + + Talks to the Rucio REST API directly over requests, so it needs no + rucio client install.""" + + homepage = "https://github.com/eic/rucio-eic-mcp-server" + git = "https://github.com/eic/rucio-eic-mcp-server.git" + + maintainers("wdconinc") + + tags = ["eic"] + + license("MIT", checked_by="aprozo") + + version("main", branch="main") + # No release tag upstream yet; pin the current main for reproducibility. + version("0.1.0", commit="e5b630bdebaa6d7156a71db5c6287d3fb425ee17") + + depends_on("python@3.10:", type=("build", "run")) + depends_on("py-setuptools@61:", type="build") + depends_on("py-wheel", type="build") + + depends_on("py-mcp+cli", type=("build", "run")) + depends_on("py-requests@2.28:", type=("build", "run")) diff --git a/spack_repo/eic/packages/supergateway/package.py b/spack_repo/eic/packages/supergateway/package.py new file mode 100644 index 00000000..838ceb50 --- /dev/null +++ b/spack_repo/eic/packages/supergateway/package.py @@ -0,0 +1,31 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.build_systems.generic import Package + +from spack.package import * + + +class Supergateway(Package): + """Run MCP stdio servers over SSE and Streamable HTTP, and vice versa.""" + + homepage = "https://github.com/supercorp-ai/supergateway" + url = "https://registry.npmjs.org/supergateway/-/supergateway-3.4.3.tgz" + + maintainers("wdconinc") + + tags = ["eic"] + + license("MIT", checked_by="aprozo") + + # The npm registry tarball ships a prebuilt dist/, so no TypeScript build + # is needed; install with --ignore-scripts to skip the (dev-only) prepare. + version("3.4.3", sha256="d26391996026bb5967a77e877474cdffa9431ed59c65186d557064de98f5ab84") + + depends_on("node-js@20:", type=("build", "run")) + depends_on("npm@10:", type="build") + + def install(self, spec, prefix): + npm = which("npm", required=True) + npm("install", "--global", "--ignore-scripts", f"--prefix={prefix}", ".") diff --git a/spack_repo/eic/packages/uproot_mcp_server/package.py b/spack_repo/eic/packages/uproot_mcp_server/package.py new file mode 100644 index 00000000..c0849345 --- /dev/null +++ b/spack_repo/eic/packages/uproot_mcp_server/package.py @@ -0,0 +1,33 @@ +# Copyright Spack Project Developers. See COPYRIGHT file for details. +# +# SPDX-License-Identifier: (Apache-2.0 OR MIT) + +from spack_repo.builtin.build_systems.python import PythonPackage + +from spack.package import * + + +class UprootMcpServer(PythonPackage): + """An MCP server for inspecting and analyzing ROOT files with uproot.""" + + homepage = "https://github.com/eic/uproot-mcp-server" + url = "https://github.com/eic/uproot-mcp-server/archive/refs/tags/v0.1.0.tar.gz" + git = "https://github.com/eic/uproot-mcp-server.git" + + maintainers("wdconinc") + + tags = ["eic"] + + license("MIT", checked_by="aprozo") + + version("main", branch="main") + version("0.1.0", sha256="b52485744f9e9112ada49c4455e51b6937a19922ca1c197be4b0d5656983b76f") + + depends_on("python@3.10:", type=("build", "run")) + depends_on("py-hatchling", type="build") + + depends_on("py-mcp", type=("build", "run")) + depends_on("py-uproot@5:", type=("build", "run")) + depends_on("py-numpy@1.26.4:", type=("build", "run")) + depends_on("py-awkward@2:", type=("build", "run")) + depends_on("py-restrictedpython@8.1:", type=("build", "run")) diff --git a/spack_repo/eic/packages/xrootd_mcp_server/package.py b/spack_repo/eic/packages/xrootd_mcp_server/package.py index 413daa14..daf7018d 100644 --- a/spack_repo/eic/packages/xrootd_mcp_server/package.py +++ b/spack_repo/eic/packages/xrootd_mcp_server/package.py @@ -29,4 +29,15 @@ class XrootdMcpServer(Package): def install(self, spec, prefix): npm = which("npm", required=True) + # Compile the TypeScript explicitly: released tags have no `prepare` + # hook, so a plain global install from source ships no build/ tree + # (and no runnable console command). + npm("install") + npm("run", "build") + # The bin entry point needs the exec bit; older build scripts skip it. + set_executable(join_path("build", "src", "index.js")) + # build/ is gitignored and package.json has no files list, so npm pack + # would drop everything but the bin/main entry; an empty .npmignore + # keeps the compiled tree in the installed package. + touch(".npmignore") npm("install", "--global", f"--prefix={prefix}", ".") diff --git a/spack_repo/eic/packages/zenodo_mcp_server/package.py b/spack_repo/eic/packages/zenodo_mcp_server/package.py index f8328b1e..86b67b0f 100644 --- a/spack_repo/eic/packages/zenodo_mcp_server/package.py +++ b/spack_repo/eic/packages/zenodo_mcp_server/package.py @@ -28,4 +28,15 @@ class ZenodoMcpServer(Package): def install(self, spec, prefix): npm = which("npm", required=True) + # Compile the TypeScript explicitly: released tags have no `prepare` + # hook, so a plain global install from source ships no build/ tree + # (and no runnable console command). + npm("install") + npm("run", "build") + # The bin entry point needs the exec bit; older build scripts skip it. + set_executable(join_path("build", "src", "index.js")) + # build/ is gitignored and package.json has no files list, so npm pack + # would drop everything but the bin/main entry; an empty .npmignore + # keeps the compiled tree in the installed package. + touch(".npmignore") npm("install", "--global", f"--prefix={prefix}", ".")