Skip to content

Added highly experimental state-mcp #3671

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 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
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
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 @@ -170,6 +170,10 @@ jobs:
{
"ID": "Build-Executor",
"Args": ["state", "run", "build-exec"]
},
{
"ID": "Build-MCP",
"Args": ["state", "run", "build-mcp"]
}
]
EOF
Expand Down Expand Up @@ -225,6 +229,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 @@ -245,6 +254,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 @@ -106,7 +111,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 @@ -121,16 +128,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 @@ -147,6 +167,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
103 changes: 103 additions & 0 deletions cmd/state-mcp/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package main

import (
"bytes"
"context"
"encoding/json"
"strings"

"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/runners/cve"
"github.com/ActiveState/cli/internal/runners/manifest"
"github.com/ActiveState/cli/internal/runners/projects"
"github.com/mark3labs/mcp-go/mcp"
)

// listProjectsHandler handles the list_projects tool
func (t *mcpServerHandler) listProjectsHandler(ctx context.Context, request mcp.CallToolRequest) (r *mcp.CallToolResult, rerr error) {
var byt bytes.Buffer
prime, close, err := t.newPrimer("", &byt)
if err != nil {
return nil, errs.Wrap(err, "Failed to create primer")
}
defer func() {
if err := close(); err != nil {
rerr = errs.Pack(rerr, err)
}
}()

runner := projects.NewProjects(prime)
params := projects.NewParams()
err = runner.Run(params)
if err != nil {
return nil, errs.Wrap(err, "Failed to run projects")
}

return mcp.NewToolResultText(byt.String()), nil
}

// manifestHandler handles the view_manifest tool
func (t *mcpServerHandler) manifestHandler(ctx context.Context, request mcp.CallToolRequest) (r *mcp.CallToolResult, rerr error) {
pjPath := request.Params.Arguments["project_directory"].(string)

var byt bytes.Buffer
prime, close, err := t.newPrimer(pjPath, &byt)
if err != nil {
return nil, errs.Wrap(err, "Failed to create primer")
}
defer func() {
if err := close(); err != nil {
rerr = errs.Pack(rerr, err)
}
}()

m := manifest.NewManifest(prime)
err = m.Run(manifest.Params{})
if err != nil {
return nil, errs.Wrap(err, "Failed to run manifest")
}

return mcp.NewToolResultText(byt.String()), nil
}

// cveHandler handles the view_cves tool
func (t *mcpServerHandler) cveHandler(ctx context.Context, request mcp.CallToolRequest) (r *mcp.CallToolResult, rerr error) {
pjPath := request.Params.Arguments["project_directory"].(string)

var byt bytes.Buffer
prime, close, err := t.newPrimer(pjPath, &byt)
if err != nil {
return nil, errs.Wrap(err, "Failed to create primer")
}
defer func() {
if err := close(); err != nil {
rerr = errs.Pack(rerr, err)
}
}()

c := cve.NewCve(prime)
err = c.Run(&cve.Params{})
if err != nil {
return nil, errs.Wrap(err, "Failed to run manifest")
}

return mcp.NewToolResultText(byt.String()), nil
}

// lookupCveHandler handles the lookup_cve tool
func (t *mcpServerHandler) lookupCveHandler(ctx context.Context, request mcp.CallToolRequest) (r *mcp.CallToolResult, rerr error) {
cveId := request.Params.Arguments["cve_ids"].(string)
cveIds := strings.Split(cveId, ",")

results, err := LookupCve(cveIds...)
if err != nil {
return nil, errs.Wrap(err, "Failed to lookup CVEs")
}

byt, err := json.Marshal(results)
if err != nil {
return nil, errs.Wrap(err, "Failed to marshal results")
}

return mcp.NewToolResultText(string(byt)), nil
}
38 changes: 38 additions & 0 deletions cmd/state-mcp/lookupcve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package main

import (
"encoding/json"
"fmt"
"net/http"

"github.com/ActiveState/cli/internal/chanutils/workerpool"
"github.com/ActiveState/cli/internal/errs"
)

func LookupCve(cveIds ...string) (map[string]interface{}, error) {
results := map[string]interface{}{}
// https://api.osv.dev/v1/vulns/OSV-2020-111
wp := workerpool.New(5)
for _, cveId := range cveIds {
wp.Submit(func() error {
resp, err := http.Get(fmt.Sprintf("https://api.osv.dev/v1/vulns/%s", cveId))
if err != nil {
return err
}
defer resp.Body.Close()
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}
results[cveId] = result
return nil
})
}

err := wp.Wait()
if err != nil {
return nil, errs.Wrap(err, "Failed to wait for workerpool")
}

return results, nil
}
43 changes: 43 additions & 0 deletions cmd/state-mcp/lookupcve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package main

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestLookupCve(t *testing.T) {
// Table-driven test cases
tests := []struct {
name string
cveIds []string
}{
{
name: "Single CVE",
cveIds: []string{"CVE-2021-44228"},
},
{
name: "Multiple CVEs",
cveIds: []string{"CVE-2021-44228", "CVE-2022-22965"},
},
{
name: "Non-existent CVE",
cveIds: []string{"CVE-DOES-NOT-EXIST"},
},
{
name: "Empty Input",
cveIds: []string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
results, err := LookupCve(tt.cveIds...)
require.NoError(t, err)
require.NotNil(t, results)
for _, cveId := range tt.cveIds {
require.Contains(t, results, cveId)
}
})
}
}
51 changes: 51 additions & 0 deletions cmd/state-mcp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package main

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

"github.com/ActiveState/cli/internal/events"
"github.com/ActiveState/cli/internal/logging"
"github.com/mark3labs/mcp-go/server"
)

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)
}
}()

mcpHandler := registerServer()

// Parse command line flags
rawFlag := flag.String("type", "", "Type of MCP server to run; raw, curated or scripts")
flag.Parse()
switch *rawFlag {
case "raw":
close := registerRawTools(mcpHandler)
defer close()
case "scripts":
close := registerScriptTools(mcpHandler)
defer close()
default:
registerCuratedTools(mcpHandler)
}

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