Skip to content

MCP Server - stdio mode only#87

Draft
thibauult wants to merge 1 commit intoopenhue:mainfrom
thibauult:feat/mcp-stdio
Draft

MCP Server - stdio mode only#87
thibauult wants to merge 1 commit intoopenhue:mainfrom
thibauult:feat/mcp-stdio

Conversation

@thibauult
Copy link
Contributor

@thibauult thibauult commented Sep 9, 2025

Summary by CodeRabbit

  • New Features

    • Introduced an AI command group in the CLI for organizing AI-related commands.
    • Added an MCP server subcommand to run a server that exposes a tool to list rooms from your Hue home.
  • Chores

    • Added MCP SDK dependency and updated indirect libraries for improved compatibility and stability.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 9, 2025

Walkthrough

Adds a new MCP server command exposing a “room_get” tool that lists Hue rooms, plus a simple “SayHi” tool. Wires the MCP server into the root CLI under an AI command group. Updates go.mod to include the MCP SDK and refreshes several indirect dependencies.

Changes

Cohort / File(s) Summary
MCP server command
cmd/mcp/server.go
Introduces an MCP server: defines HiParams, tools SayHi and ListRooms, and NewCmdMcpServer to run the server over stdio with OpenHUE context; registers tool room_get.
Root command wiring
cmd/root.go
Adds AI command group OpenHueCmdGroupAI; registers mcp.NewCmdMcpServer(ctx) as a subcommand; reorganizes imports.
Dependencies
go.mod
Adds github.com/modelcontextprotocol/go-sdk v0.2.0; adds github.com/yosida95/uritemplate/v3 indirectly; bumps several golang.org/x/* indirect dependencies.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant CLI as Cobra CLI
    participant MCP as MCP Server
    participant Tool as Tool: room_get
    participant OH as OpenHUE Context

    rect rgba(230,245,255,0.6)
    note over CLI: New AI group and mcp-server command
    User->>CLI: openhue mcp-server
    CLI->>MCP: Start over stdio (with OH in context)
    end

    rect rgba(235,255,235,0.6)
    note over MCP: Tool invocation flow
    User->>MCP: Call tool "room_get"
    MCP->>Tool: Invoke ListRooms(ctx)
    Tool->>OH: Get rooms
    alt Rooms found
        OH-->>Tool: [room list]
        Tool-->>MCP: Formatted room names
        MCP-->>User: Content: list of rooms
    else No rooms
        OH-->>Tool: []
        Tool-->>MCP: "No rooms found"
        MCP-->>User: Message: none
    else Context error
        Tool-->>MCP: error (no OpenHUE context)
        MCP-->>User: Error response
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks (1 passed, 2 warnings)

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description Check ⚠️ Warning The pull request currently has no description, so it fails to summarize or contextualize any of the changes made in the code and does not convey the intent or scope of the update. Please add a pull request description that summarizes the key functional additions—such as the new MCP server command, its stdio transport mode, and the introduced SayHi and ListRooms tools—to clearly communicate the scope and purpose of the changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title Check ✅ Passed The title references a core aspect of the changeset by indicating the addition of an MCP server operating in stdio mode, which is directly related to the main functionality introduced and remains concise and specific.

Poem

I twitched my ears at the humming wire,
A new MCP server sparks to fire.
“room_get,” I hop—list rooms in sight,
With OpenHUE to guide the light.
Dependencies trimmed, commands aligned—
Carrot-ready, code refined.
Hippity-hop, ship it on time!

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@thibauult thibauult marked this pull request as draft September 9, 2025 13:25
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (5)
go.mod (2)

41-48: Tidy and lock X packages to the module’s Go version.

After upgrading x/* libs, run go mod tidy -compat=1.23 to ensure graph minimality and avoid accidental reliance on 1.24-only stdlib APIs when go 1.23.0 is declared.


12-12: Bump MCP SDK to v0.4.0

- github.com/modelcontextprotocol/go-sdk v0.2.0
+ github.com/modelcontextprotocol/go-sdk v0.4.0

v0.4.0 (released 2025-09-05) includes the latest stdio transport and AddTool enhancements; upgrade to pull in these non-breaking improvements.

cmd/mcp/server.go (3)

3-11: Alias the SDK import to avoid confusion with local package name.

The file’s package is mcp and it imports .../mcp. Aliasing improves readability and avoids mental collisions.

Apply:

-import (
+import (
 	"context"
 	"fmt"
 	"openhue-cli/openhue"
 	"strings"
 
-	"github.com/modelcontextprotocol/go-sdk/mcp"
+	mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
 	"github.com/spf13/cobra"
 )

Then update type refs accordingly (see next diffs).


13-21: Register the “SayHi” tool or remove dead code.

SayHi is defined but never registered; either wire it or delete. Wiring is useful for smoke-testing MCP.

Also convert SDK types to the aliased import:

 type HiParams struct {
 	Name string `json:"name" jsonschema:"the name of the person to greet"`
 }
 
-func SayHi(ctx context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[HiParams]) (*mcp.CallToolResultFor[any], error) {
-	return &mcp.CallToolResultFor[any]{
-		Content: []mcp.Content{&mcp.TextContent{Text: "Hi " + params.Arguments.Name}},
+func SayHi(ctx context.Context, cc *mcpsdk.ServerSession, params *mcpsdk.CallToolParamsFor[HiParams]) (*mcpsdk.CallToolResultFor[any], error) {
+	return &mcpsdk.CallToolResultFor[any]{
+		Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: "Hi " + params.Arguments.Name}},
 	}, nil
 }

And in Run, add the tool (see later diff).


17-47: Use a typed, unexported context key instead of a string.

Prevents collisions and makes retrieval compile-time safe.

Apply:

 type HiParams struct {
 	Name string `json:"name" jsonschema:"the name of the person to greet"`
 }
 
+// context key for *openhue.Context (typed to avoid collisions)
+type ctxKey int
+const ctxKeyOpenHue ctxKey = iota
+
-func ListRooms(c context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[any]) (*mcp.CallToolResultFor[any], error) {
+func ListRooms(c context.Context, cc *mcpsdk.ServerSession, params *mcpsdk.CallToolParamsFor[any]) (*mcpsdk.CallToolResultFor[any], error) {
 
-	ctx, ok := c.Value("openhue").(*openhue.Context)
+	ctx, ok := c.Value(ctxKeyOpenHue).(*openhue.Context)
@@
-		return &mcp.CallToolResultFor[any]{
-			Content: []mcp.Content{&mcp.TextContent{Text: "I couldn't find any rooms in your Hue home."}},
+		return &mcpsdk.CallToolResultFor[any]{
+			Content: []mcpsdk.Content{&mcpsdk.TextContent{Text: "I couldn't find any rooms in your Hue home."}},
 		}, nil
 	}
@@
-	return &mcp.CallToolResultFor[any]{
-		Content: []mcp.Content{&mcp.TextContent{
+	return &mcpsdk.CallToolResultFor[any]{
+		Content: []mcpsdk.Content{&mcpsdk.TextContent{
 			Text: fmt.Sprintf("Here are the rooms I found: %s.", strings.Join(roomNames, ", ")),
 		}},
 	}, nil
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97f54a3 and 949b708.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (3)
  • cmd/mcp/server.go (1 hunks)
  • cmd/root.go (4 hunks)
  • go.mod (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
cmd/mcp/server.go (3)
openhue/context.go (1)
  • Context (9-15)
openhue/home_service.go (1)
  • SearchRooms (67-83)
openhue/buildinfo.go (1)
  • BuildInfo (3-7)
cmd/root.go (2)
cmd/mcp/server.go (1)
  • NewCmdMcpServer (50-73)
cmd/setup/setup.go (1)
  • NewCmdSetup (21-43)
🔇 Additional comments (2)
cmd/root.go (2)

24-26: Group ID addition looks good.

AI group constant is clear and consistent with the new command’s GroupID.


54-57: LGTM: AI command group registration.

This enables discoverability without impacting Hue/config groups.

Comment on lines +23 to +31
func ListRooms(c context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[any]) (*mcp.CallToolResultFor[any], error) {

ctx, ok := c.Value("openhue").(*openhue.Context)
if !ok || ctx == nil {
return nil, fmt.Errorf("could not retrieve openhue context")
}

rooms := openhue.SearchRooms(ctx.Home, nil)
if len(rooms) == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Avoid nil deref when Home isn’t preloaded (ctx.Home can be nil).

SearchRooms(ctx.Home, ...) will panic if the HomeModel wasn’t loaded (the AI group doesn’t trigger LoadHomeIfNeeded). Lazily load it here.

Apply:

 func ListRooms(c context.Context, cc *mcp.ServerSession, params *mcp.CallToolParamsFor[any]) (*mcp.CallToolResultFor[any], error) {

-	ctx, ok := c.Value("openhue").(*openhue.Context)
+	ctx, ok := c.Value(ctxKeyOpenHue).(*openhue.Context)
 	if !ok || ctx == nil {
 		return nil, fmt.Errorf("could not retrieve openhue context")
 	}
 
-	rooms := openhue.SearchRooms(ctx.Home, nil)
+	// Lazy-load Home if needed
+	if ctx.Home == nil {
+		home, err := openhue.LoadHome(ctx.H)
+		if err != nil {
+			return nil, fmt.Errorf("failed to load Hue home: %w", err)
+		}
+		ctx.Home = home
+	}
+	rooms := openhue.SearchRooms(ctx.Home, nil)

Note: This diff references a typed context key added in a separate suggestion below.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cmd/mcp/server.go around lines 23 to 31, the call SearchRooms(ctx.Home, ...)
can panic if ctx.Home is nil because Home may not be preloaded; before calling
SearchRooms, check if ctx.Home == nil and lazily load it (invoke the context's
LoadHomeIfNeeded or equivalent method), handle and return any load error, and
only then call SearchRooms with the now-non-nil Home to avoid a nil dereference.

Comment on lines +49 to +73
// NewCmdMcpServer returns an initialized Command instance for 'mcp-server' sub command
func NewCmdMcpServer(ctx *openhue.Context) *cobra.Command {

cmd := &cobra.Command{
Use: "mcp-server",
Aliases: []string{"mcp"},
Short: "Starts the MCP server",
GroupID: "ai",
Run: func(cmd *cobra.Command, args []string) {

// Create a server instance
server := mcp.NewServer(&mcp.Implementation{Name: "openhue", Version: ctx.BuildInfo.Version}, nil)

// Add a tool to the server
mcp.AddTool(server, &mcp.Tool{Name: "room_get", Description: "List all rooms from home"}, ListRooms)

// Run the server over stdin/stdout, until the client disconnects
if err := server.Run(context.WithValue(context.Background(), "openhue", ctx), mcp.NewStdioTransport()); err != nil {
cobra.CheckErr(err)
}
},
}

return cmd
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Harden the command for stdio: silence Cobra output, set stderr, add SayHi tool, and use typed context key.

Prevents protocol corruption and completes the tool set.

Apply:

 func NewCmdMcpServer(ctx *openhue.Context) *cobra.Command {
 
 	cmd := &cobra.Command{
 		Use:     "mcp-server",
 		Aliases: []string{"mcp"},
 		Short:   "Starts the MCP server",
 		GroupID: "ai",
+		SilenceUsage:  true,
+		SilenceErrors: true,
 		Run: func(cmd *cobra.Command, args []string) {
 
 			// Create a server instance
-			server := mcp.NewServer(&mcp.Implementation{Name: "openhue", Version: ctx.BuildInfo.Version}, nil)
+			version := "dev"
+			if ctx != nil && ctx.BuildInfo != nil && ctx.BuildInfo.Version != "" {
+				version = ctx.BuildInfo.Version
+			}
+			server := mcpsdk.NewServer(&mcpsdk.Implementation{Name: "openhue", Version: version}, nil)
 
 			// Add a tool to the server
-			mcp.AddTool(server, &mcp.Tool{Name: "room_get", Description: "List all rooms from home"}, ListRooms)
+			mcpsdk.AddTool(server, &mcpsdk.Tool{Name: "room_get", Description: "List all rooms from home"}, ListRooms)
+			mcpsdk.AddTool(server, &mcpsdk.Tool{Name: "say_hi", Description: "Greet a user"}, SayHi)
 
 			// Run the server over stdin/stdout, until the client disconnects
-			if err := server.Run(context.WithValue(context.Background(), "openhue", ctx), mcp.NewStdioTransport()); err != nil {
+			if err := server.Run(context.WithValue(context.Background(), ctxKeyOpenHue, ctx), mcpsdk.NewStdioTransport()); err != nil {
 				cobra.CheckErr(err)
 			}
 		},
 	}
 
+	// Ensure Cobra writes help/usage to stderr (not stdout)
+	cmd.SetOut(cmd.ErrOrStderr())
 	return cmd
 }

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +96 to 97
root.AddCommand(mcp.NewCmdMcpServer(ctx))

Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Protect MCP stdio from accidental stdout noise.

Because the MCP server speaks over stdout, any Cobra/print output there can corrupt the stream. Ensure all logs/usage/errors go to stderr and silence Cobra’s auto-usage.

Apply in the MCP command constructor (see server.go suggestions): set SilenceUsage and SilenceErrors to true and set command output to ErrOrStderr(). Also double‑check no code prints to stdout along this code path.

🤖 Prompt for AI Agents
In cmd/root.go around lines 96-97, the MCP subcommand is added but Cobra may
write usage/errors to stdout which will corrupt the MCP stdout protocol; in the
MCP command constructor (server.go) set cmd.SilenceUsage = true and
cmd.SilenceErrors = true, set the command’s output to
cmd.SetOut(cmd.ErrOrStderr()), and audit the MCP command code path to replace
any fmt.Print/Println or other stdout writes with stderr equivalents or
cmd.ErrOrStderr() so nothing writes to stdout.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant