Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1e30204
Merge pull request #3629 from ActiveState/version/0-47-0-RC2
Naatan Jan 14, 2025
44f50ff
Merge pull request #3639 from ActiveState/version/0-47-0-RC3
Naatan Feb 10, 2025
abb5937
Merge pull request #3642 from ActiveState/version/0-47-0-RC3
Naatan Feb 11, 2025
cde656c
Merge pull request #3645 from ActiveState/version/0-47-1-RC1
Naatan Feb 21, 2025
5a667f6
Merge branch 'version/0-47-1-RC1' into beta
Naatan Mar 3, 2025
e5d7281
Merge remote-tracking branch 'origin/master' into mcp
Naatan Apr 8, 2025
1271598
Captain: fix SetArgs, sort of
Naatan Apr 10, 2025
e8d1d28
Don't include root command (state) in NameRecursive
Naatan Apr 10, 2025
94c8042
Added BaseCommand and AllChildren to Captain
Naatan Apr 10, 2025
48850be
Remove dependency on HOME and TEMPDIR env vars
Naatan Apr 10, 2025
7114a62
Support debugging via go-build
Naatan Apr 10, 2025
0d405d8
Fix nil panic
Naatan Apr 10, 2025
d614458
Fix simple output oddly formatted
Naatan Apr 10, 2025
e35097b
Drop useless debug entry
Naatan Apr 10, 2025
3a78c3c
Work around vscode syntax parsing issue
Naatan Apr 10, 2025
ed26dc9
Add state-mcp
Naatan Apr 10, 2025
7f33790
Fix AI deciding to indent something that didn't need it
Naatan Apr 10, 2025
993a88b
Fix prime being stale
Naatan Apr 10, 2025
0a062a3
Drop unused import
Naatan Apr 10, 2025
c11c5c4
Disable tests
Naatan Apr 10, 2025
c8dc1b3
Add missing comma
Naatan Apr 10, 2025
af5769a
Update test now that we work without HOME set
Naatan Apr 10, 2025
93a5529
Include state-mcp in update
Naatan Apr 10, 2025
5e29acd
Windows specific build target
Naatan Apr 10, 2025
c714877
Always print what we're building
Naatan Apr 11, 2025
5cc036d
Debug copy error
Naatan Apr 11, 2025
6286ad5
Re-org by AI
Naatan Apr 11, 2025
1d20586
Fix window target not being used
Naatan Apr 11, 2025
0063d6f
Implemented flawed script runner that doesn't pass back stdout as sub…
Naatan Apr 11, 2025
121eb84
Cleanup state-mcp
Naatan Jul 11, 2025
cac3faf
Merge branch 'master' into mcp
Naatan Jul 11, 2025
397c278
Handle cast library returning nil values if source is nil
Naatan Jul 15, 2025
6d41be7
Drop debugging code
Naatan Jul 15, 2025
38e51d5
Fix test not constructing outputter properly
Naatan Jul 15, 2025
c97f5d8
Fix tests
Naatan Jul 15, 2025
8ffadb2
Drop donotshipme package; we can in fact ship this now
Naatan Jul 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
10 changes: 10 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,10 @@ jobs:
{
"ID": "Build-Executor",
"Args": ["state", "run", "build-exec"]
},
{
"ID": "Build-MCP",
"Args": ["state", "run", "build-mcp"]
}
]
EOF
Expand Down Expand Up @@ -226,6 +230,11 @@ jobs:
shell: bash
run: parallelize results Build-Executor

- # === "Build: MCP" ===
name: "Build: MCP"
shell: bash
run: parallelize results Build-MCP

- # === Prepare Windows Cert ===
name: Prepare Windows Cert
shell: bash
Expand All @@ -246,6 +255,7 @@ jobs:
signtool.exe sign -d "ActiveState State Service" -f "Cert.p12" -p ${CODE_SIGNING_PASSWD} ./build/state-svc.exe
signtool.exe sign -d "ActiveState State Installer" -f "Cert.p12" -p ${CODE_SIGNING_PASSWD} ./build/state-installer.exe
signtool.exe sign -d "ActiveState State Tool Remote Installer" -f "Cert.p12" -p ${CODE_SIGNING_PASSWD} ./build/state-remote-installer.exe
signtool.exe sign -d "ActiveState State MCP" -f "Cert.p12" -p ${CODE_SIGNING_PASSWD} ./build/state-mcp.exe
env:
CODE_SIGNING_PASSWD: ${{ secrets.CODE_SIGNING_PASSWD }}

Expand Down
2 changes: 2 additions & 0 deletions activestate.windows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ constants:
value: state-exec.exe
- name: BUILD_INSTALLER_TARGET
value: state-installer.exe
- name: BUILD_MCP_TARGET
value: state-mcp.exe
- name: SVC_BUILDFLAGS
value: -ldflags="-s -w -H=windowsgui"
- name: SCRIPT_EXT
Expand Down
30 changes: 26 additions & 4 deletions activestate.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ constants:
value: ./cmd/state-installer
- name: EXECUTOR_PKGS
value: ./cmd/state-exec
- name: MCP_PKGS
value: ./cmd/state-mcp
- name: BUILD_TARGET_PREFIX_DIR
value: ./build
- name: BUILD_TARGET
Expand All @@ -29,6 +31,9 @@ constants:
value: state-installer
- name: BUILD_REMOTE_INSTALLER_TARGET
value: state-remote-installer
- name: BUILD_MCP_TARGET
if: ne .OS.Name "Windows"
value: state-mcp
- name: INTEGRATION_TEST_REGEX
value: 'integration\|automation'
- name: SET_ENV
Expand Down Expand Up @@ -104,7 +109,9 @@ scripts:
go generate
popd > /dev/null
fi
go build -tags "$GO_BUILD_TAGS" -o $BUILD_TARGET_DIR/$constants.BUILD_TARGET $constants.CLI_BUILDFLAGS $constants.CLI_PKGS
TARGET=$BUILD_TARGET_DIR/$constants.BUILD_TARGET
echo "Building $TARGET"
go build -tags "$GO_BUILD_TAGS" -o $TARGET $constants.CLI_BUILDFLAGS $constants.CLI_PKGS
- name: build-svc
language: bash
standalone: true
Expand All @@ -119,16 +126,29 @@ scripts:
go generate
popd > /dev/null
fi
go build -tags "$GO_BUILD_TAGS" -o $BUILD_TARGET_DIR/$constants.BUILD_DAEMON_TARGET $constants.SVC_BUILDFLAGS $constants.DAEMON_PKGS
TARGET=$BUILD_TARGET_DIR/$constants.BUILD_DAEMON_TARGET
echo "Building $TARGET"
go build -tags "$GO_BUILD_TAGS" -o $TARGET $constants.SVC_BUILDFLAGS $constants.DAEMON_PKGS
- name: build-exec
description: Builds the State Executor application
language: bash
standalone: true
value: |
set -e
$constants.SET_ENV

go build -tags "$GO_BUILD_TAGS" -o $BUILD_TARGET_DIR/$constants.BUILD_EXEC_TARGET $constants.CLI_BUILDFLAGS $constants.EXECUTOR_PKGS
TARGET=$BUILD_TARGET_DIR/$constants.BUILD_EXEC_TARGET
echo "Building $TARGET"
go build -tags "$GO_BUILD_TAGS" -o $TARGET $constants.CLI_BUILDFLAGS $constants.EXECUTOR_PKGS
- name: build-mcp
description: Builds the State MCP application
language: bash
standalone: true
value: |
set -e
$constants.SET_ENV
TARGET=$BUILD_TARGET_DIR/$constants.BUILD_MCP_TARGET
echo "Building $TARGET"
go build -tags "$GO_BUILD_TAGS" -o $TARGET $constants.CLI_BUILDFLAGS $constants.MCP_PKGS
- name: build-all
description: Builds all our tools
language: bash
Expand All @@ -145,6 +165,8 @@ scripts:
$scripts.build-svc.path()
echo "Building State Executor"
$scripts.build-exec.path()
echo "Building State MCP"
$scripts.build-mcp.path()
- name: build-installer
language: bash
standalone: true
Expand Down
6 changes: 1 addition & 5 deletions cmd/state-installer/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -475,11 +475,7 @@ func storeInstallSource(installSource string) {
installSource = "state-installer"
}

appData, err := storage.AppDataPath()
if err != nil {
multilog.Error("Could not store install source due to AppDataPath error: %s", errs.JoinMessage(err))
return
}
appData := storage.AppDataPath()
if err := fileutils.WriteFile(filepath.Join(appData, constants.InstallSourceFile), []byte(installSource)); err != nil {
multilog.Error("Could not store install source due to WriteFile error: %s", errs.JoinMessage(err))
}
Expand Down
74 changes: 74 additions & 0 deletions cmd/state-mcp/internal/mcpserver/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package mcpserver

import (
"context"
"fmt"

"github.com/ActiveState/cli/internal/constants"
"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/logging"
"github.com/ActiveState/cli/internal/primer"
"github.com/mark3labs/mcp-go/mcp"
"github.com/mark3labs/mcp-go/server"
)

type ToolHandlerFunc func(context.Context, *primer.Values, mcp.CallToolRequest) (*mcp.CallToolResult, error)

// Handler wraps the MCP server and provides methods for adding tools and resources
type Handler struct {
Server *server.MCPServer
primeGetter func() (*primer.Values, func() error, error)
}

func New(primeGetter func() (*primer.Values, func() error, error)) *Handler {
s := server.NewMCPServer(
constants.StateMCPCmd,
constants.VersionNumber,
)

mcpHandler := &Handler{
Server: s,
primeGetter: primeGetter,
}

return mcpHandler
}

func (m Handler) ServeStdio() error {
if err := server.ServeStdio(m.Server); err != nil {
logging.Error("Server error: %v\n", err)
}
return nil
}

// addResource adds a resource to the MCP server with error handling and logging
func (m *Handler) AddResource(resource mcp.Resource, handler server.ResourceHandlerFunc) {
m.Server.AddResource(resource, func(ctx context.Context, request mcp.ReadResourceRequest) ([]mcp.ResourceContents, error) {
r, err := handler(ctx, request)
if err != nil {
logging.Error("%s: Error handling resource request: %v", resource.Name, err)
return nil, errs.Wrap(err, "Failed to handle resource request")
}
return r, nil
})
}

// addTool adds a tool to the MCP server with error handling and logging
func (m *Handler) AddTool(tool mcp.Tool, handler ToolHandlerFunc) {
m.Server.AddTool(tool, func(ctx context.Context, request mcp.CallToolRequest) (r *mcp.CallToolResult, rerr error) {
p, closer, err := m.primeGetter()
if err != nil {
return nil, errs.Wrap(err, "Failed to get primer")
}
defer closer()
r, err = handler(ctx, p, request)
if err != nil {
logging.Error("%s: Error handling tool request: %v", tool.Name, errs.JoinMessage(err))
// Format all errors as a single string, so the client gets the full context
return nil, fmt.Errorf("%s: %s", tool.Name, errs.JoinMessage(err))
}
return r, nil
})
}


23 changes: 23 additions & 0 deletions cmd/state-mcp/internal/toolregistry/categories.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package toolregistry

type ToolCategory string

const (
ToolCategoryDebug ToolCategory = "debug"
)

type ToolCategories []ToolCategory

func (c ToolCategories) String() []string {
result := []string{}
for _, category := range c {
result = append(result, string(category))
}
return result
}

func Categories() ToolCategories {
return ToolCategories{
ToolCategoryDebug,
}
Comment on lines +19 to +22
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Usually we like to return pointers to avoid copies each time this function is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've gotten into the habit of only using pointers when I intend to track the underlying type across different areas of responsibility. I don't believe we ever had any rule around this.

I'd have preferred to make this a constant, but Go is a little weird about what things can be constants.

}
56 changes: 56 additions & 0 deletions cmd/state-mcp/internal/toolregistry/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package toolregistry

import (
"context"
"slices"

"github.com/ActiveState/cli/internal/primer"
"github.com/mark3labs/mcp-go/mcp"
)

type Tool struct {
mcp.Tool
Category ToolCategory
Handler func(context.Context, *primer.Values, mcp.CallToolRequest) (*mcp.CallToolResult, error)
}

type Registry struct {
tools map[ToolCategory][]Tool
}

func New() *Registry {
r := &Registry{
tools: make(map[ToolCategory][]Tool),
}

r.RegisterTool(HelloWorldTool())

return r
}

func (r *Registry) RegisterTool(tool Tool) {
if _, ok := r.tools[tool.Category]; !ok {
r.tools[tool.Category] = []Tool{}
}
r.tools[tool.Category] = append(r.tools[tool.Category], tool)
}

func (r *Registry) GetTools(requestCategories ...string) []Tool {
if len(requestCategories) == 0 {
for _, category := range Categories() {
if category == ToolCategoryDebug {
// Debug must be explicitly requested
continue
}
requestCategories = append(requestCategories, string(category))
}
}
categories := Categories()
result := []Tool{}
for _, category := range categories {
if slices.Contains(requestCategories, string(category)) {
result = append(result, r.tools[category]...)
}
}
return result
}
40 changes: 40 additions & 0 deletions cmd/state-mcp/internal/toolregistry/tools.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package toolregistry

import (
"context"
"strings"

"github.com/ActiveState/cli/internal/primer"
"github.com/ActiveState/cli/internal/runners/hello"
"github.com/mark3labs/mcp-go/mcp"
)

func HelloWorldTool() Tool {
return Tool{
Category: ToolCategoryDebug,
Tool: mcp.NewTool(
"hello",
mcp.WithDescription("Hello world tool"),
mcp.WithString("name", mcp.Required(), mcp.Description("The name to say hello to")),
),
Handler: func(ctx context.Context, p *primer.Values, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
name, err := request.RequireString("name")
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

runner := hello.New(p)
params := hello.NewParams()
params.Name = name

err = runner.Run(params)
if err != nil {
return mcp.NewToolResultError(err.Error()), nil
}

return mcp.NewToolResultText(
strings.Join(p.Output().History().Print, "\n"),
), nil
},
}
}
55 changes: 55 additions & 0 deletions cmd/state-mcp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package main

import (
"flag"
"fmt"
"os"
"runtime/debug"
"strings"
"time"

"github.com/ActiveState/cli/cmd/state-mcp/internal/mcpserver"
"github.com/ActiveState/cli/cmd/state-mcp/internal/toolregistry"
"github.com/ActiveState/cli/internal/events"
"github.com/ActiveState/cli/internal/logging"
)

func main() {
defer func() {
logging.Debug("Exiting")
if r := recover(); r != nil {
logging.Error("Recovered from panic: %v", r)
fmt.Printf("Recovered from panic: %v, stack: %s\n", r, string(debug.Stack()))
os.Exit(1)
}
}()
defer func() {
if err := events.WaitForEvents(5*time.Second, logging.Close); err != nil {
logging.Warning("Failed waiting for events: %v", err)
}
}()

// Parse command line flags
rawFlag := flag.String("categories", "", "Comma separated list of categories to register tools for")
flag.Parse()

mcps := setupServer(strings.Split(*rawFlag, ",")...)

// Start the stdio server
logging.Info("Starting MCP server")
if err := mcps.ServeStdio(); err != nil {
logging.Error("Server error: %v\n", err)
}
}

func setupServer(categories ...string) *mcpserver.Handler {
mcps := mcpserver.New(newPrimer)

registry := toolregistry.New()
tools := registry.GetTools(categories...)
for _, tool := range tools {
mcps.AddTool(tool.Tool, tool.Handler)
}

return mcps
}
Loading
Loading