Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
.claude/
bin/
.DS_Store
110 changes: 110 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

Trend Vision One MCP Server - A Go-based Model Context Protocol (MCP) server that bridges AI tooling (Claude, VSCode + GitHub Copilot) with Trend Vision One security platform APIs. Enables natural language interaction with security services like Workbench alerts, Cloud Posture, endpoint management, attack surface discovery, and AI security guardrails.

## Build & Test Commands

```bash
# Build
make mcpserver # Build to ./bin/v1-mcp-server
go build -o ./bin/v1-mcp-server ./cmd/v1-mcp-server/main.go

# Test
go test -v ./... # Run all tests

# Lint & Format
./script/check-gofmt # Check formatting
./script/lint # Run golangci-lint
gofmt -s -w ./ # Auto-format code

# Run locally
./bin/v1-mcp-server -region us # Requires TREND_VISION_ONE_API_KEY env var
```

## Architecture

```
cmd/v1-mcp-server/main.go # Entry point, CLI flags, region validation
internal/v1mcp/server.go # MCP server setup, tool registration
internal/v1mcp/tools/*.go # Tool handlers (one file per domain)
internal/v1client/*.go # HTTP client, API endpoint methods
```

**Request Flow:** MCP Request → Tool Handler → Parameter Extraction → v1client API call → HTTP → Response → MCP Result

## Key Conventions

### Tool Implementation Pattern
Each tool is a factory function returning `mcpserver.ServerTool`:
```go
func toolDomainResourceAction(client *v1client.V1ApiClient) mcpserver.ServerTool {
return mcpserver.ServerTool{
Tool: mcp.NewTool("domain_resource_action",
mcp.WithDescription("..."),
mcp.WithString("param", mcp.Description("...")),
mcp.WithToolAnnotation(mcp.ToolAnnotation{ReadOnlyHint: toPtr(true)}),
),
Handler: func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) {
// Extract parameters, call API, return result
},
}
}
```

### Read-Only vs Write Tools
- Tools must be annotated with `ReadOnlyHint: toPtr(true/false)`
- Server validates annotations match toolset registration (panics on mismatch)
- Add read tools to `ToolsetsReadOnly{Domain}`, write tools to `ToolsetsWrite{Domain}` in respective `tools/*.go` files
- Register toolsets in `server.go`

### API Paths
Paths must NOT start with `/`. The client's `Parse` method handles URL joining:
```go
// Correct
c.searchAndFilter("v3.0/iam/apiKeys", filter, params)
// Wrong
c.searchAndFilter("/v3.0/iam/apiKeys", filter, params)
```

### Parameter Helpers (tools/tools.go)
```go
requiredValue[T](property string, vals map[string]any) (T, error)
optionalValue[T](property string, vals map[string]any) (T, error)
optionalIntValue(property string, vals map[string]any) (int, error)
optionalTimeValue(property string, vals map[string]any) (time.Time, error)
handleStatusResponse(r *http.Response, err error, expectedStatusCode int, msg string) (*mcp.CallToolResult, error)
```

### Request Options (v1client/v1client.go)
```go
withHeader(name, value string) requestOptionFunc // Add custom headers to requests
```
Used by domains requiring custom headers (e.g., AI Security uses `TMV1-Application-Name`, `TMV1-Request-Type`, `Prefer`).

### Tool Naming Convention
`{domain}_{resource}_{action}` - e.g., `iam_api_keys_list`, `workbench_alert_detail_get`

## Domain Organization

| Domain | Client File | Tools File | API Prefix |
|--------|-------------|------------|------------|
| AI Security | `v1client/aisecurity.go` | `tools/aisecurity.go` | `v3.0/aiSecurity/` |
| IAM | `v1client/iam.go` | `tools/iam.go` | `v3.0/iam/` |
| Workbench | `v1client/workbench.go` | `tools/workbench.go` | `v3.0/workbench/` |
| OAT | `v1client/oat.go` | `tools/workbench.go` | `v3.0/oat/` |
| Cloud Posture | `v1client/cloudposture.go` | `tools/cloudposture.go` | `v3.0/asrm/` |
| CREM | `v1client/crem.go` | `tools/crem.go` | `v3.0/asrm/` |
| CAM | `v1client/cam.go` | `tools/cam.go` | `v3.0/cam/` |
| Email | `v1client/email.go` | `tools/email.go` | `v3.0/email/` |
| Container | `v1client/container.go` | `tools/container.go` | `v3.0/containerSecurity/` |
| Endpoint | `v1client/endpoint.go` | `tools/endpoint.go` | `v3.0/endpointSecurity/` |

**Note:** OAT (Observed Attack Techniques) has its own client file but tools are registered under the Workbench toolset.

## Contributing

Follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,12 @@ Alternatively, copy the following into your `settings.json`.
| `endpoint_security_tasks_list` | Displays the tasks of your endpoints in a paginated list | `read` |
| `endpoint_security_version_control_policies_list` | Displays your Endpoint Version Control policies | `read` |

### AI Security

| Tool | Description | Mode |
| ---- | ----------- | ---- |
| `aisecurity_guardrails_apply` | Evaluates prompts against AI guard policies and returns the recommended action (Allow/Block) with reasons for any policy violations detected | `read` |

## Architecture

![high-level architecture](./doc/images/trend-vision-one-mcp.png)
Expand Down
11 changes: 8 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,21 @@ module github.com/trendmicro/vision-one-mcp-server
go 1.23.0

require (
github.com/google/go-querystring v1.1.0
github.com/mark3labs/mcp-go v0.27.0
github.com/google/go-querystring v1.2.0
github.com/mark3labs/mcp-go v0.43.2
github.com/stretchr/testify v1.9.0
)

require (
github.com/bahlo/generic-list-go v0.2.0 // indirect
github.com/buger/jsonparser v1.1.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/invopop/jsonschema v0.13.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
28 changes: 18 additions & 10 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,31 +1,39 @@
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.2.0 h1:yhqkPbu2/OH+V9BfpCVPZkNmUXhb2gBxJArfhIxNtP0=
github.com/google/go-querystring v1.2.0/go.mod h1:8IFJqpSRITyJ8QhQ13bmbeMBDfmeEJZD5A0egEOmkqU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E=
github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mark3labs/mcp-go v0.27.0 h1:iok9kU4DUIU2/XVLgFS2Q9biIDqstC0jY4EQTK2Erzc=
github.com/mark3labs/mcp-go v0.27.0/go.mod h1:rXqOudj/djTORU/ThxYx8fqEVj/5pvTuuebQ2RC7uk4=
github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8=
github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mark3labs/mcp-go v0.43.2 h1:21PUSlWWiSbUPQwXIJ5WKlETixpFpq+WBpbMGDSVy/I=
github.com/mark3labs/mcp-go v0.43.2/go.mod h1:YnJfOL382MIWDx1kMY+2zsRHU/q78dBg9aFb8W6Thdw=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc=
github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw=
github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
88 changes: 88 additions & 0 deletions internal/v1client/aisecurity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package v1client

import (
"bytes"
"encoding/json"
"net/http"
)

// AISecurityApplyGuardrailsInput represents the input for the applyGuardrails API.
// It supports three request types:
// - SimpleRequestGuard: just a prompt string
// - OpenAIChatCompletionRequestV1: OpenAI chat completion request format
// - OpenAIChatCompletionResponseV1: OpenAI chat completion response format
type AISecurityApplyGuardrailsInput struct {
// For SimpleRequestGuard
Prompt string `json:"prompt,omitempty"`

// For OpenAIChatCompletionRequestV1
Model string `json:"model,omitempty"`
Messages []AISecurityChatMessage `json:"messages,omitempty"`

// For OpenAIChatCompletionResponseV1
ID string `json:"id,omitempty"`
Object string `json:"object,omitempty"`
Created int64 `json:"created,omitempty"`
Choices []AISecurityChatChoice `json:"choices,omitempty"`
Usage *AISecurityUsage `json:"usage,omitempty"`
}

// AISecurityChatMessage represents a message in the chat conversation.
type AISecurityChatMessage struct {
Role string `json:"role"`
Content string `json:"content"`
}

// AISecurityChatChoice represents a choice in the OpenAI chat completion response.
type AISecurityChatChoice struct {
Index int `json:"index"`
Message AISecurityChoiceMessage `json:"message"`
FinishReason string `json:"finish_reason"`
}

// AISecurityChoiceMessage represents a message in a chat choice.
type AISecurityChoiceMessage struct {
Role string `json:"role"`
Content string `json:"content"`
Refusal *string `json:"refusal,omitempty"`
}

// AISecurityUsage represents token usage statistics.
type AISecurityUsage struct {
PromptTokens int `json:"prompt_tokens"`
CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
}

// AISecurityApplyGuardrailsOptions contains the optional headers for the applyGuardrails API.
type AISecurityApplyGuardrailsOptions struct {
// ApplicationName is required - the name of the AI application whose prompts are being evaluated
ApplicationName string
// RequestType is the type of request being evaluated (SimpleRequestGuard, OpenAIChatCompletionRequestV1, OpenAIChatCompletionResponseV1)
RequestType string
// Prefer controls response detail level (return=representation for detailed, return=minimal for short)
Prefer string
}

// AISecurityApplyGuardrails evaluates prompts against AI guard policies.
func (c *V1ApiClient) AISecurityApplyGuardrails(input AISecurityApplyGuardrailsInput, opts AISecurityApplyGuardrailsOptions) (*http.Response, error) {
b, err := json.Marshal(&input)
if err != nil {
return nil, err
}

r, err := c.newRequest(
http.MethodPost,
"v3.0/aiSecurity/applyGuardrails",
bytes.NewReader(b),
withContentTypeJSON(),
withHeader("TMV1-Application-Name", opts.ApplicationName),
withHeader("TMV1-Request-Type", opts.RequestType),
withHeader("Prefer", opts.Prefer),
)
if err != nil {
return nil, err
}

return c.client.Do(r)
}
10 changes: 10 additions & 0 deletions internal/v1client/v1client.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ func contentTypeJSON(r *http.Request) {
r.Header.Add("content-type", "application/json")
}

// withHeader adds a custom header to the request.
func withHeader(name, value string) requestOptionFunc {
return func(r *http.Request) {
if value == "" {
return
}
r.Header.Add(name, value)
}
}

func (c *V1ApiClient) searchAndFilter(path, filter string, queryParams any) (*http.Response, error) {
p, err := query.Values(queryParams)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/v1mcp/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func NewMcpServer(cfg ServerConfig) (*mcpserver.MCPServer, error) {
addReadOnlyToolset(s, client, tools.ToolsetsReadOnlyEmail)
addReadOnlyToolset(s, client, tools.ToolsetsReadOnlyContainer)
addReadOnlyToolset(s, client, tools.ToolsetsReadOnlyEndpoint)
addReadOnlyToolset(s, client, tools.ToolsetsReadOnlyAISecurity)

if !cfg.ReadOnly {
addWriteToolset(s, client, tools.ToolsetsWriteCloudPosture)
Expand Down
Loading
Loading