Skip to content
Closed
44 changes: 44 additions & 0 deletions spack_repo/eic/packages/eic_mcp/package.py
Original file line number Diff line number Diff line change
@@ -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"))
56 changes: 56 additions & 0 deletions spack_repo/eic/packages/opencode/package.py
Original file line number Diff line number Diff line change
@@ -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"))
29 changes: 29 additions & 0 deletions spack_repo/eic/packages/py_httpx_sse/package.py
Original file line number Diff line number Diff line change
@@ -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"))
47 changes: 47 additions & 0 deletions spack_repo/eic/packages/py_mcp/package.py
Original file line number Diff line number Diff line change
@@ -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"))
21 changes: 21 additions & 0 deletions spack_repo/eic/packages/py_pyjwt/package.py
Original file line number Diff line number Diff line change
@@ -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"))
26 changes: 26 additions & 0 deletions spack_repo/eic/packages/py_restrictedpython/package.py
Original file line number Diff line number Diff line change
@@ -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")
29 changes: 29 additions & 0 deletions spack_repo/eic/packages/py_sse_starlette/package.py
Original file line number Diff line number Diff line change
@@ -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"))
34 changes: 34 additions & 0 deletions spack_repo/eic/packages/rucio_eic_mcp_server/package.py
Original file line number Diff line number Diff line change
@@ -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"))
31 changes: 31 additions & 0 deletions spack_repo/eic/packages/supergateway/package.py
Original file line number Diff line number Diff line change
@@ -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}", ".")
33 changes: 33 additions & 0 deletions spack_repo/eic/packages/uproot_mcp_server/package.py
Original file line number Diff line number Diff line change
@@ -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"))
11 changes: 11 additions & 0 deletions spack_repo/eic/packages/xrootd_mcp_server/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}", ".")
11 changes: 11 additions & 0 deletions spack_repo/eic/packages/zenodo_mcp_server/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}", ".")
Loading