Skip to content

feat: add spark client module for SparkConnect sessions#51

Open
vedhakoushik wants to merge 5 commits into
kubeflow:mainfrom
vedhakoushik:feat/spark-client-module
Open

feat: add spark client module for SparkConnect sessions#51
vedhakoushik wants to merge 5 commits into
kubeflow:mainfrom
vedhakoushik:feat/spark-client-module

Conversation

@vedhakoushik

Copy link
Copy Markdown

What

Implements the spark client module (Closes #5), wrapping kubeflow.spark.SparkClient so AI agents can manage SparkConnect sessions over MCP. Structure mirrors the existing trainer module (api/, types/, resources/).

Tools (5)

Tool Kind SDK method
list_spark_sessions read-only SparkClient.list_sessions()
get_spark_session read-only SparkClient.get_session()
get_spark_session_logs read-only SparkClient.get_session_logs()
create_spark_session write (confirm-gate) SparkClient.connect() (create mode)
delete_spark_session write, destructive (confirm-gate) SparkClient.delete_session()

Both write tools use the repo's two-phase confirm gate (confirmed=False previews, confirmed=True executes).

Design notes / decisions for reviewers

  • Only the public, JSON-serializable SparkClient methods are wrapped. All kubeflow.spark / pyspark imports are lazy, so the module and its metadata load even without the optional kubeflow[spark] extra (missing extra → friendly SDK_ERROR telling the user to install it). This keeps --clients spark importable on any host.
  • create_spark_session uses the SDK's only public creation path, connect(), which returns a live pyspark SparkSession. The tool releases that transient session (.stop()) and returns session metadata; the data plane attaches with PySpark using the returned connect info — the MCP server never proxies Spark RPCs. If connect() fails after the CR is created (e.g. the host can't reach the driver), the tool still reports the provisioned session with a warning rather than losing it. A provision-only SDK method would remove the host-reachability requirement — noted as a follow-up in spark://guides/troubleshooting.
  • Out of scope (documented as known limitations): attaching to an existing server (connect(base_url=...) returns a non-serializable live session) and follow=True log streaming. SparkApplication batch jobs are not part of the SDK's SparkClient today.
  • Wiring: new spark_* tool phases; personas updated (readonly → read tools, data-scientist → lifecycle); delete_spark_session added to DESTRUCTIVE_TOOLS; module registered in CLIENT_MODULES and selectable via --clients trainer,spark.
  • One existing test changedtests/unit/trainer/test_architecture.py::test_all_phases_mapped. It previously asserted the trainer module's PHASE_TO_SECTION maps every global phase, an invariant only valid while trainer was the sole client module. The server already merges each module's PHASE_TO_SECTION, so I scoped the test to trainer's own phases. Flagging explicitly since it touches an existing test.

Testing

  • 33 new unit tests (tests/unit/spark/): metadata/annotation consistency, phase + persona + destructive wiring, SparkConnectInfo serialization (enum/datetime), and mocked-client behavior for all 5 tools (state filter, not-found → RESOURCE_NOT_FOUND, log tailing/truncation, confirm-gate previews, validation).
  • Full suite: 73 passed; ruff check and ruff format clean.
  • End-to-end: server loads trainer+spark and registers all 5 tools in both full and progressive modes.

Docs

ARCHITECTURE.md and README.md updated; two agent-facing guides added under spark/resources/ (session-patterns.md, troubleshooting.md).


🤖 Authored with Claude Code. First-time contributor — happy to adjust scope, the creation approach, or naming to match maintainer preferences.

Copilot AI review requested due to automatic review settings July 9, 2026 14:45
@google-oss-prow

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign abhijeet-dhumal for approval. For more information see the Kubernetes Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🎉 Welcome to the Kubeflow MCP Server! 🎉

Thanks for opening your first PR! We're happy to have you as part of our community 🚀

Here's what happens next:

  • If you haven't already, please check out our Contributing Guide for repo-specific guidelines and the Kubeflow Contributor Guide for general community standards
  • Our team will review your PR soon! cc @kubeflow/kubeflow-sdk-team

Join the community:

Feel free to ask questions in the comments if you need any help or clarification!
Thanks again for contributing to Kubeflow! 🙏

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new spark client module to the Kubeflow MCP server, exposing SparkConnect session lifecycle + monitoring tools via the same modular architecture used by the existing trainer client.

Changes:

  • Introduces kubeflow_mcp.spark with 5 MCP tools (list/get/logs/create/delete) plus resources and serialization helpers.
  • Wires the spark module into server registration, tool phases, personas, and destructive-tool policy.
  • Adds a comprehensive unit-test suite for the spark module and adjusts an existing trainer architecture test to be module-scoped.

Reviewed changes

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/unit/trainer/test_architecture.py Scopes phase-to-section assertions to phases actually used by trainer tools.
tests/unit/spark/test_spark_module.py New unit tests covering spark tool metadata, wiring, and mocked behavior.
tests/unit/spark/init.py Initializes spark unit test package.
README.md Documents spark as a selectable client module and updates CLI example.
kubeflow_mcp/spark/types/init.py Adds JSON-safe serialization helper for SparkConnectInfo-like objects.
kubeflow_mcp/spark/resources/troubleshooting.md Adds spark-specific troubleshooting guide.
kubeflow_mcp/spark/resources/session-patterns.md Adds agent-facing usage patterns for SparkConnect sessions.
kubeflow_mcp/spark/api/sessions.py Implements create/delete tools with confirm-gating and validation.
kubeflow_mcp/spark/api/monitoring.py Implements bounded log retrieval tool with truncation behavior.
kubeflow_mcp/spark/api/discovery.py Implements list/get discovery tools and “next steps” guidance.
kubeflow_mcp/spark/api/init.py Declares spark API package.
kubeflow_mcp/spark/init.py Defines spark module metadata, tool registration, annotations, resources, and instruction sections.
kubeflow_mcp/core/server.py Registers spark in CLIENT_MODULES.
kubeflow_mcp/core/policy.py Adds spark tools to persona policies and destructive-tool set.
kubeflow_mcp/common/utils.py Adds lazy SparkClient factories with per-namespace caching.
kubeflow_mcp/common/constants.py Adds spark phases/tools mapping and marks spark capability as implemented.
kubeflow_mcp/cli.py Updates --clients help text to include spark.
ARCHITECTURE.md Updates architecture matrix to reflect spark module availability.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +207 to +227
def get_spark_client_for_namespace(namespace: str | None = None) -> Any:
"""Return a SparkClient targeting the given namespace.

When *namespace* is ``None`` the shared singleton (default kubeconfig
namespace) is returned. When a namespace is explicitly provided a cached
``SparkClient`` scoped to that namespace is returned.
"""
if namespace is None:
return get_spark_client()
with _spark_ns_client_lock:
if namespace in _spark_ns_client_cache:
return _spark_ns_client_cache[namespace]
spark_client_cls = _import_spark_client()
from kubeflow.common.types import KubernetesBackendConfig

client = spark_client_cls(backend_config=KubernetesBackendConfig(namespace=namespace))
if len(_spark_ns_client_cache) >= _NS_CLIENT_CACHE_MAX:
oldest = next(iter(_spark_ns_client_cache))
del _spark_ns_client_cache[oldest]
_spark_ns_client_cache[namespace] = client
return client

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 7d93f9e. get_spark_client_for_namespace(None) now resolves the effective namespace via the same resolver check_namespace_allowed uses (get_trainer_effective_namespace) and scopes the SparkClient to it, so the policy-checked namespace always matches the one Spark operates in — falling back to the unscoped singleton only if resolution fails. Added two unit tests covering the resolve and fallback paths.

Comment thread README.md Outdated
Comment on lines 158 to 162
kubeflow-mcp serve \
--clients trainer \ # modules: trainer, optimizer (stub), hub (stub)
--clients trainer,spark \ # modules: trainer, spark, optimizer (stub), hub (stub)
--persona ml-engineer \ # readonly | data-scientist | ml-engineer | platform-admin
--mode full \ # full | progressive | semantic
--instruction-tier full \ # full | compact | minimal

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 7d93f9e. The kubeflow-mcp serve example is now a clean, copy/paste-safe command (no trailing \ followed by inline # comments), with the per-flag options moved into a table right below it.

@tariq-hasan

Copy link
Copy Markdown
Member

Thanks for raising the PR @vedhakoushik!

Regarding the implementation I had two questions @andreyvelich @abhijeet-dhumal.

  • Should we planning to break the implementation scope into multiple PRs spanning SparkConnect, SparkJob etc. - given that feat: [KEP-107] Add Spark batch job submission and lifecycle APIs sdk#521 is already underway and plans to add support for SparkJob - or do we intend to keep it all focused on one PR?
  • Are there any hard dependencies that need to be addressed or merged first or any sequencing that needs to be followed or are we good enough to proceed at this time?

@abhijeet-dhumal

Copy link
Copy Markdown
Member

Thanks for the Spark client work. Before review, please link a design doc or KEP reference (Phase 6 / #5) describing the tool surface and scope. XXL PRs need an accepted design per CONTRIBUTING.md — happy to review once that's linked.

/hold

@google-oss-prow google-oss-prow Bot added the do-not-merge/hold Blocked — do not merge label Jul 10, 2026
@vedhakoushik

vedhakoushik commented Jul 10, 2026

Copy link
Copy Markdown
Author

Thanks @abhijeet-dhumal! I've added a design doc describing the tool surface and scope, committed to this PR and linked from ARCHITECTURE.md:

docs/design/spark-client.mdhttps://github.com/vedhakoushik/mcp-server/blob/feat/spark-client-module/docs/design/spark-client.md

It references the SDK design (KEP-107 / kubeflow/sdk#107), enumerates the five SparkConnect tools and their SDK mapping, and records the design decisions (lazy kubeflow[spark] imports, create semantics via SparkClient.connect(), namespace safety, and mirroring the trainer module). It also documents the intended scope — SparkConnect session management in this PR.

Happy to adjust anything, or reshape it into a KEP-style proposal if you'd prefer that format. Ready for review whenever you get a chance — thanks!

@abhijeet-dhumal

abhijeet-dhumal commented Jul 10, 2026

Copy link
Copy Markdown
Member

Thanks @abhijeet-dhumal! I've added a design doc describing the tool surface and scope, committed to this PR and linked from ARCHITECTURE.md:

docs/design/spark-client.mdhttps://github.com/vedhakoushik/mcp-server/blob/feat/spark-client-module/docs/design/spark-client.md

It references the SDK design (KEP-107 / kubeflow/sdk#107), enumerates the five SparkConnect tools and their SDK mapping, and records the design decisions (lazy kubeflow[spark] imports, create semantics via SparkClient.connect(), namespace safety, and mirroring the trainer module). It also documents the intended scope — SparkConnect session management in this PR.

Happy to adjust anything, or reshape it into a KEP-style proposal if you'd prefer that format. Ready for review whenever you get a chance — thanks!

@vedhakoushik I would suggest to start with a proposal like this : #48
and also make sure you consider stack compatibility metrics too.. for example this project is progressing on a specific compatibility metrics ( Kubeflow trainer api, Kubeflow SDK v0.4.0) .. so we should be aligning our implementation accordingly..

@vedhakoushik

Copy link
Copy Markdown
Author

Thanks @abhijeet-dhumal, that's really helpful — I went through #48 / KEP-34.

Since the Spark work here is scoped to SparkConnect and already lines up with the project's compatibility baseline (kubeflow[spark] >= 0.4.0, which is where SparkClient for SparkConnect landed — SDK 0.4 / kubeflow/sdk#225), I'd like to add a KEP-style proposal following the KEP-34 structure, including the SDK-compatibility metrics (unified SDK ≥ 0.4.0, SparkConnect CRD sparkoperator.k8s.io/v1alpha1, pyspark↔cluster-Spark version match, K8s ≥ 1.27, Python ≥ 3.10).

Before I push, two quick questions so I match your preference:

  1. Would you prefer the proposal added to this PR (docs restructured to the KEP format), or as a separate proposal-only PR like chore(docs): katib (optimizer) client support for kubeflow-mcp-server (kep#34) #48?
  2. What KEP number should I use for the Spark proposal? (I didn't want to guess and collide with KEP-34.)

Scope stays SparkConnect-only; Spark batch jobs (SparkJob) are a follow-up once kubeflow/sdk#521 lands. Happy to adjust anything — thanks for the guidance!

@vedhakoushik

Copy link
Copy Markdown
Author

Opened the proposal as a separate PR, mirroring #48: #52 — KEP-5: SparkConnect (Spark) Client Support.

It follows the KEP-34 structure and includes the SDK-compatibility metrics (kubeflow SDK ≥ 0.4.0, SparkConnect CRD sparkoperator.k8s.io/v1alpha1, pyspark↔cluster-Spark version match, Kubeflow Trainer ≥ v2.2.0, K8s ≥ 1.27, Python ≥ 3.10). For the KEP number, I followed the same convention KEP-34 uses — the tracking issue number (#34 → KEP-34, so #5 → KEP-5) — happy to renumber if you'd prefer differently.

This PR (#51) is the reference implementation linked from the proposal.

vedhakoushik added a commit to vedhakoushik/mcp-server that referenced this pull request Jul 11, 2026
All four verified against the actual code/SDK before fixing:

- Serialize the released SDK field name, not main. Downloaded and inspected
  the real kubeflow==0.4.0 and 0.4.1 wheels from PyPI: SparkConnectInfo names
  the driver-pod field `pod_name` on both released versions; `driver_pod_name`
  only exists on unreleased SDK main (I had read GitHub's default branch, not
  a tagged release). Design now specifies pod_name first, driver_pod_name
  fallback, with a matching test. This also means the reference implementation
  on kubeflow#51 has a real bug (getattr(..., "driver_pod_name", None) silently
  returns None against the declared >=0.4.0 baseline) that needs a follow-up
  fix there.

- Ownership guard on delete_spark_session. Confirmed the trainer module's
  precedent (is_mcp_managed() / MCP_MANAGED_LABEL in common/utils.py,
  enforced in trainer/api/lifecycle.py, label attached at creation in
  trainer/api/training.py). The Spark design had no equivalent, so any
  persona with delete access could remove any SparkConnect session in an
  allowed namespace, not just MCP-created ones. Design now mirrors the
  trainer pattern.

- get_session_logs() log-bounding is client-side only. Confirmed via the
  SDK's backend.py: read_namespaced_pod_log() is called without a
  server-side tail limit, so the full log is materialized before our
  tail_lines truncation applies. Reworded tool descriptions to stop
  overclaiming "bounded" and added a mitigation (bypass the SDK wrapper for
  a true server-side tail, matching trainer's existing direct-CoreV1Api
  precedent in discovery.py).

- Out-of-cluster port-forward leak on create_spark_session. Confirmed via
  the SDK's KubernetesBackend.connect(): outside the cluster it spawns a
  kubectl port-forward subprocess that SparkSession.stop() does not
  terminate. Documented as a known limitation with a recommended mitigation
  (in-cluster deployment).

Updated: SDK Compatibility table + note, Goals, Key Design Decisions (2 new
bullets + 1 extended), Non-Goals, MCP Tools tables, Persona Coverage, SDK
Compatibility Update mapping, Risks and Mitigations (+3 rows), Testing Plan.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Bellam vedha kousik <bellam.vedhakoushik@gmail.com>
@abhijeet-dhumal

Copy link
Copy Markdown
Member

/assign @vedhakoushik

vedhakoushik and others added 5 commits July 12, 2026 19:39
Implement the `spark` client module wrapping kubeflow.spark.SparkClient so
AI agents can manage SparkConnect sessions over MCP, mirroring the trainer
module's architecture.

Tools (5):
- list_spark_sessions / get_spark_session / get_spark_session_logs (read-only)
- create_spark_session / delete_spark_session (write, two-phase confirm gate)

Design notes:
- Only the public, JSON-serializable SparkClient methods are wrapped. All
  kubeflow.spark / pyspark imports are lazy, so the module and its metadata
  load without the optional `kubeflow[spark]` extra.
- create_spark_session uses the SDK's only public creation path (connect()),
  releases the transient driver SparkSession, and returns session metadata;
  the data plane attaches with PySpark. Documented in resources + Known
  Limitations (attach-to-existing and follow=True streaming are out of scope).
- New spark_* tool phases; personas and DESTRUCTIVE_TOOLS updated; module
  registered in CLIENT_MODULES and selectable via `--clients trainer,spark`.
- Agent-facing guides added under spark/resources (session-patterns,
  troubleshooting); ARCHITECTURE.md / README updated.

Testing:
- 33 unit tests (metadata, wiring, serialization, mocked-client behavior).
- Updated trainer test_all_phases_mapped: each client module now owns/maps its
  own phases, so the invariant is scoped to trainer's own tools.
- Full unit suite (73 tests) + ruff check/format pass; server loads and
  registers all 5 tools in full and progressive modes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bellam vedha kousik <bellam.vedhakoushik@gmail.com>
- README: make the `kubeflow-mcp serve` example copy/paste-safe. The prior
  block ended each line with `\` followed by spaces and an inline `#` comment,
  so the backslash did not escape the newline and the command broke when pasted.
  Replace inline comments with a clean runnable command + an options table.
- common/utils: when namespace is None, resolve the effective namespace via the
  same resolver check_namespace_allowed uses (get_trainer_effective_namespace)
  and scope the SparkClient to it, so the policy-checked namespace always matches
  the one Spark operates in (closes a potential namespace-allowlist bypass).
  Falls back to the unscoped singleton if resolution fails.
- Add 2 unit tests for the factory namespace-resolution behavior (75 total).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bellam vedha kousik <bellam.vedhakoushik@gmail.com>
Add a design doc describing the spark module's tool surface and scope, as
requested in review before merging an XXL PR. Covers the five SparkConnect
tools, the SDK mapping (KEP-107), design decisions (lazy imports, create-via-
connect semantics, namespace safety), testing, and — importantly — the scope
split: this PR is SparkConnect-only; Spark batch jobs (SparkJob) are a follow-up
gated on kubeflow/sdk#521. Linked from ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bellam vedha kousik <bellam.vedhakoushik@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bellam vedha kousik <bellam.vedhakoushik@gmail.com>
Two Step-A fixes ahead of the KEP-5 design work landing:

- kubeflow_mcp/spark/types/__init__.py: session_info_to_dict() read
  driver_pod_name via getattr, which silently returns None against the
  declared kubeflow[spark]>=0.4.0 baseline — the released 0.4.0/0.4.1
  wheels name the field pod_name; driver_pod_name only exists on
  unreleased SDK main. Read pod_name first, fall back to
  driver_pod_name, so session info is correct today and keeps working
  if the rename ships in a future release. Added regression tests for
  both field shapes.

- Removed docs/design/spark-client.md, superseded by the KEP-5 design
  proposal (kubeflow#52, docs/proposals/KEP-5.md in that PR). Repointed
  ARCHITECTURE.md's Spark row to kubeflow#52.

Rebased onto upstream/main to resolve the README.md merge conflict
(upstream added --otel-endpoint to the serve example; kept our
copy/paste-safe table format and folded the new flag into it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Bellam vedha kousik <bellam.vedhakoushik@gmail.com>
@vedhakoushik
vedhakoushik force-pushed the feat/spark-client-module branch from 973a53f to bace2f4 Compare July 12, 2026 19:03
@vedhakoushik

Copy link
Copy Markdown
Author

Thanks for the assignment! Pushed an update to clear the merge conflict and fix a bug the KEP-5 review surfaced:

  • Rebased onto the latest main (resolved the README.md conflict from the OTel --otel-endpoint flag).
  • Fixed session_info_to_dict(): it was reading driver_pod_name, which only exists on unreleased SDK main — the released kubeflow[spark]==0.4.0/0.4.1 (our declared baseline) names the field pod_name. Now reads pod_name first with a driver_pod_name fallback, with regression tests for both shapes.
  • Removed the superseded docs/design/spark-client.md; ARCHITECTURE.md now points at chore(docs): sparkconnect (spark) client support for kubeflow-mcp-server (kep#5) #52 (KEP-5).

Still on hold pending #52. Once that's accepted I'll bring the remaining KEP-5 commitments (ownership guard on delete_spark_session, server-side log tail bound, TOOL_NEXT_HINTS, and spark_pre_flight) into this PR — happy to split spark_pre_flight into a follow-up if that's preferable given the size here.

@vedhakoushik

Copy link
Copy Markdown
Author

Hi @abhijeet-dhumal — Two quick questions as a first-time contributor here:

  1. Is KEP-5 (chore(docs): sparkconnect (spark) client support for kubeflow-mcp-server (kep#5) #52 ready for /lgtm + /approve, or would you like more changes?
  2. For feat: add spark client module for SparkConnect sessions #51: should spark_pre_flight + the other KEP additions go into that PR directly, or land as a smaller follow-up (it's already size/XXL)?

Happy to go either way — just want to follow the right process. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

do-not-merge/hold Blocked — do not merge size/XXL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

SparkClient module for Kubeflow MCP Server

4 participants