Skip to content

Commit 5c4df43

Browse files
MacAttakclaude
andauthored
feat(codegen): wire entrypoint execution into CLI and MCP generators (#22)
* feat(codegen): wire CLI generator entrypoint execution with cliArgs construction Replace the echo stub in the Go CLI template with real exec.CommandContext calls to the manifest's entrypoint path. Build a cliArgs slice following the runner convention: positional args first, flags in definition order, auth token last. Task 1 of codegen-entrypoint-wiring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(codegen): wire MCP TypeScript generator entrypoint execution with args construction Replace the TODO stub in the MCP tool template with real execFile calls to the manifest's entrypoint path. Build an args array following the runner convention: positional args first, flags in definition order, auth token last via CLI flag (constitution rule 24). Task 2 of codegen-entrypoint-wiring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * test(codegen): add integration tests for entrypoint wiring across generators Verify generated CLI and MCP projects use real entrypoints instead of echo/TODO stubs. Cover per-tool entrypoint isolation, cliArgs/args construction, token passing via CLI flags, no-auth tool isolation, and TypeScript brace balance. Task 3 of codegen-entrypoint-wiring. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(codegen): resolve PR review findings — dead code guard, unused import - Use local variable for entrypoint in both CLI and MCP templates to eliminate constant-expression dead code (staticcheck SA9003 / ESLint) - Remove unused validateRequest import from MCP tool template — it's HTTP middleware not applicable in MCP tool handlers - Update 3 CLI test assertions to match variable-based pattern Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: MacAttak <MacAttak@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0d8177a commit 5c4df43

File tree

5 files changed

+2113
-87
lines changed

5 files changed

+2113
-87
lines changed

internal/codegen/cli_go.go

Lines changed: 122 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,8 @@ type flagData struct {
207207

208208
type argData struct {
209209
Name string // original arg name used in string literals
210-
GoName string // sanitized Go identifier
210+
GoName string // sanitized Go identifier (camelCase, lowercase first)
211+
GoNameCap string // GoName with first letter uppercased (PascalCase)
211212
GoType string
212213
Required bool
213214
Description string
@@ -224,10 +225,11 @@ type toolGoData struct {
224225
AuthType string
225226
TokenEnv string
226227
TokenFlag string
227-
HasNonStringArgs bool // true if any arg needs strconv parsing
228-
HasNonStringArrayFlags bool // true if any flag is a non-string array type needing strconv
229-
HasObjectFlags bool // true if any flag is type "object" or "object[]"
230-
IsBinaryOutput bool // true if tool output format is "binary"
228+
HasNonStringArgs bool // true if any arg needs strconv parsing
229+
HasNonStringArrayFlags bool // true if any flag is a non-string array type needing strconv
230+
HasObjectFlags bool // true if any flag is type "object" or "object[]"
231+
IsBinaryOutput bool // true if tool output format is "binary"
232+
Entrypoint string // executable path from manifest tool.Entrypoint
231233
}
232234

233235
type goModData struct {
@@ -317,9 +319,15 @@ func toolSummaries(m manifest.Toolkit) []toolSummary {
317319
func buildToolData(m manifest.Toolkit, tool manifest.Tool, auth manifest.Auth) toolGoData {
318320
args := make([]argData, len(tool.Args))
319321
for i, a := range tool.Args {
322+
gn := goIdentifier(a.Name)
323+
gnCap := gn
324+
if len(gn) > 0 {
325+
gnCap = strings.ToUpper(gn[:1]) + gn[1:]
326+
}
320327
args[i] = argData{
321328
Name: a.Name,
322-
GoName: goIdentifier(a.Name),
329+
GoName: gn,
330+
GoNameCap: gnCap,
323331
GoType: goType(a.Type),
324332
Required: a.Required,
325333
Description: a.Description,
@@ -413,6 +421,7 @@ func buildToolData(m manifest.Toolkit, tool manifest.Tool, auth manifest.Auth) t
413421
HasNonStringArrayFlags: hasNonStringArrayFlags,
414422
HasObjectFlags: hasObjectFlags,
415423
IsBinaryOutput: tool.Output.Format == "binary",
424+
Entrypoint: tool.Entrypoint,
416425
}
417426
}
418427

@@ -694,6 +703,7 @@ import (
694703
{{- $authType := .AuthType}}
695704
{{- $tokenEnv := .TokenEnv}}
696705
{{- $tokenFlag := .TokenFlag}}
706+
{{- $entrypoint := .Entrypoint}}
697707
var (
698708
{{- range .Flags}}
699709
{{- if .IsArray}}
@@ -740,114 +750,158 @@ var {{.GoName}}Cmd = &cobra.Command{
740750
RunE: func(cmd *cobra.Command, args []string) error {
741751
{{- range $i, $a := .Args}}
742752
{{- if eq $a.GoType "string"}}
743-
arg{{$a.GoName}} := args[{{$i}}] // {{$a.GoType}}
753+
arg{{$a.GoNameCap}} := args[{{$i}}] // {{$a.GoType}}
744754
{{- else if eq $a.GoType "int"}}
745-
arg{{$a.GoName}}, err := strconv.Atoi(args[{{$i}}]) // {{$a.GoType}}
755+
arg{{$a.GoNameCap}}, err := strconv.Atoi(args[{{$i}}]) // {{$a.GoType}}
746756
if err != nil {
747757
return fmt.Errorf("invalid value %q for argument {{$a.Name}}: %w", args[{{$i}}], err)
748758
}
749759
{{- else if eq $a.GoType "float64"}}
750-
arg{{$a.GoName}}, err := strconv.ParseFloat(args[{{$i}}], 64) // {{$a.GoType}}
760+
arg{{$a.GoNameCap}}, err := strconv.ParseFloat(args[{{$i}}], 64) // {{$a.GoType}}
751761
if err != nil {
752762
return fmt.Errorf("invalid value %q for argument {{$a.Name}}: %w", args[{{$i}}], err)
753763
}
754764
{{- else if eq $a.GoType "bool"}}
755-
arg{{$a.GoName}}, err := strconv.ParseBool(args[{{$i}}]) // {{$a.GoType}}
765+
arg{{$a.GoNameCap}}, err := strconv.ParseBool(args[{{$i}}]) // {{$a.GoType}}
756766
if err != nil {
757767
return fmt.Errorf("invalid value %q for argument {{$a.Name}}: %w", args[{{$i}}], err)
758768
}
759769
{{- end}}
760-
_ = arg{{$a.GoName}}
770+
{{- end}}
771+
{{- if $hasAuth}}
772+
// Resolve auth token: prefer flag, fall back to env var.
773+
token := {{$goName}}Token
774+
if token == "" {
775+
token = os.Getenv("{{$tokenEnv | esc}}")
776+
}
777+
if token == "" {
778+
return fmt.Errorf("auth required: set {{$tokenEnv | esc}} or pass --{{$tokenFlag | esc}}")
779+
}
780+
{{- end}}
781+
entrypoint := "{{$entrypoint | esc}}"
782+
if entrypoint == "" {
783+
return fmt.Errorf("{{$toolName}}: entrypoint not configured")
784+
}
785+
var cliArgs []string
786+
{{- range $i, $a := .Args}}
787+
{{- if eq $a.GoType "string"}}
788+
cliArgs = append(cliArgs, arg{{$a.GoNameCap}})
789+
{{- else if eq $a.GoType "int"}}
790+
cliArgs = append(cliArgs, fmt.Sprintf("%d", arg{{$a.GoNameCap}}))
791+
{{- else if eq $a.GoType "float64"}}
792+
cliArgs = append(cliArgs, fmt.Sprintf("%g", arg{{$a.GoNameCap}}))
793+
{{- else if eq $a.GoType "bool"}}
794+
cliArgs = append(cliArgs, strconv.FormatBool(arg{{$a.GoNameCap}}))
795+
{{- end}}
761796
{{- end}}
762797
{{- range .Flags}}
763798
{{- if .IsArray}}
764-
{{- if eq .ArrayBase "int"}}
765-
parsed{{.GoName}} := make([]int, len({{$goName}}Flag{{.GoName}}))
766-
for i, s := range {{$goName}}Flag{{.GoName}} {
767-
v, err := strconv.Atoi(s)
768-
if err != nil {
769-
return fmt.Errorf("invalid value %q for element of --{{.Name}}: not a valid int", s)
799+
{{- if eq .ArrayBase "string"}}
800+
for _, v := range {{$goName}}Flag{{.GoName}} {
801+
cliArgs = append(cliArgs, "--{{.Name}}", v)
802+
}
803+
{{- else if eq .ArrayBase "int"}}
804+
{
805+
parsed{{.GoName}} := make([]int, len({{$goName}}Flag{{.GoName}}))
806+
for i, s := range {{$goName}}Flag{{.GoName}} {
807+
v, err := strconv.Atoi(s)
808+
if err != nil {
809+
return fmt.Errorf("invalid value %q for element of --{{.Name}}: not a valid int", s)
810+
}
811+
parsed{{.GoName}}[i] = v
812+
}
813+
for _, v := range parsed{{.GoName}} {
814+
cliArgs = append(cliArgs, "--{{.Name}}", strconv.Itoa(v))
770815
}
771-
parsed{{.GoName}}[i] = v
772816
}
773-
_ = parsed{{.GoName}}
774817
{{- else if eq .ArrayBase "float"}}
775-
parsed{{.GoName}} := make([]float64, len({{$goName}}Flag{{.GoName}}))
776-
for i, s := range {{$goName}}Flag{{.GoName}} {
777-
v, err := strconv.ParseFloat(s, 64)
778-
if err != nil {
779-
return fmt.Errorf("invalid value %q for element of --{{.Name}}: not a valid float", s)
818+
{
819+
parsed{{.GoName}} := make([]float64, len({{$goName}}Flag{{.GoName}}))
820+
for i, s := range {{$goName}}Flag{{.GoName}} {
821+
v, err := strconv.ParseFloat(s, 64)
822+
if err != nil {
823+
return fmt.Errorf("invalid value %q for element of --{{.Name}}: not a valid float", s)
824+
}
825+
parsed{{.GoName}}[i] = v
826+
}
827+
for _, v := range parsed{{.GoName}} {
828+
cliArgs = append(cliArgs, "--{{.Name}}", fmt.Sprintf("%g", v))
780829
}
781-
parsed{{.GoName}}[i] = v
782830
}
783-
_ = parsed{{.GoName}}
784831
{{- else if eq .ArrayBase "bool"}}
785-
parsed{{.GoName}} := make([]bool, len({{$goName}}Flag{{.GoName}}))
786-
for i, s := range {{$goName}}Flag{{.GoName}} {
787-
v, err := strconv.ParseBool(s)
788-
if err != nil {
789-
return fmt.Errorf("invalid value %q for element of --{{.Name}}: not a valid bool", s)
832+
{
833+
parsed{{.GoName}} := make([]bool, len({{$goName}}Flag{{.GoName}}))
834+
for i, s := range {{$goName}}Flag{{.GoName}} {
835+
v, err := strconv.ParseBool(s)
836+
if err != nil {
837+
return fmt.Errorf("invalid value %q for element of --{{.Name}}: not a valid bool", s)
838+
}
839+
parsed{{.GoName}}[i] = v
840+
}
841+
for _, v := range parsed{{.GoName}} {
842+
cliArgs = append(cliArgs, "--{{.Name}}", strconv.FormatBool(v))
790843
}
791-
parsed{{.GoName}}[i] = v
792844
}
793-
_ = parsed{{.GoName}}
794-
{{- end}}
795845
{{- end}}
796-
{{- end}}
797-
{{- range .Flags}}
798-
{{- if .IsObject}}
846+
{{- else if .IsObject}}
847+
if {{$goName}}Flag{{.GoName}} != "" {
799848
{{- if .IsObjectArray}}
800-
var parsed{{.GoName}} []map[string]any
801-
if err := json.Unmarshal([]byte({{$goName}}Flag{{.GoName}}), &parsed{{.GoName}}); err != nil {
802-
return fmt.Errorf("invalid JSON for --{{.Name}}: %w", err)
803-
}
849+
var parsed{{.GoName}} []map[string]any
850+
if err := json.Unmarshal([]byte({{$goName}}Flag{{.GoName}}), &parsed{{.GoName}}); err != nil {
851+
return fmt.Errorf("invalid JSON for --{{.Name}}: %w", err)
852+
}
804853
{{- if .HasItemSchema}}
805-
for _idx, _elem := range parsed{{.GoName}} {
806-
for _, _field := range []string{ {{joinQuoted .ItemSchemaProperties}} } {
807-
if _, ok := _elem[_field]; !ok {
808-
return fmt.Errorf("--{{.Name}}[%d]: required field %q missing from JSON object", _idx, _field)
854+
for _idx, _elem := range parsed{{.GoName}} {
855+
for _, _field := range []string{ {{joinQuoted .ItemSchemaProperties}} } {
856+
if _, ok := _elem[_field]; !ok {
857+
return fmt.Errorf("--{{.Name}}[%d]: required field %q missing from JSON object", _idx, _field)
858+
}
809859
}
810860
}
811-
}
812861
{{- end}}
813862
{{- else}}
814-
var parsed{{.GoName}} map[string]any
815-
if err := json.Unmarshal([]byte({{$goName}}Flag{{.GoName}}), &parsed{{.GoName}}); err != nil {
816-
return fmt.Errorf("invalid JSON for --{{.Name}}: %w", err)
817-
}
863+
var parsed{{.GoName}} map[string]any
864+
if err := json.Unmarshal([]byte({{$goName}}Flag{{.GoName}}), &parsed{{.GoName}}); err != nil {
865+
return fmt.Errorf("invalid JSON for --{{.Name}}: %w", err)
866+
}
818867
{{- if .HasItemSchema}}
819-
for _, _field := range []string{ {{joinQuoted .ItemSchemaProperties}} } {
820-
if _, ok := parsed{{.GoName}}[_field]; !ok {
821-
return fmt.Errorf("--{{.Name}}: required field %q missing from JSON object", _field)
868+
for _, _field := range []string{ {{joinQuoted .ItemSchemaProperties}} } {
869+
if _, ok := parsed{{.GoName}}[_field]; !ok {
870+
return fmt.Errorf("--{{.Name}}: required field %q missing from JSON object", _field)
871+
}
822872
}
823-
}
824873
{{- end}}
825874
{{- end}}
826-
_ = parsed{{.GoName}}
875+
cliArgs = append(cliArgs, "--{{.Name}}", {{$goName}}Flag{{.GoName}})
876+
}
877+
{{- else if eq .GoType "bool"}}
878+
if {{$goName}}Flag{{.GoName}} {
879+
cliArgs = append(cliArgs, "--{{.Name}}")
880+
}
881+
{{- else if eq .GoType "string"}}
882+
if {{$goName}}Flag{{.GoName}} != "" {
883+
cliArgs = append(cliArgs, "--{{.Name}}", {{$goName}}Flag{{.GoName}})
884+
}
885+
{{- else if eq .GoType "int"}}
886+
cliArgs = append(cliArgs, "--{{.Name}}", fmt.Sprintf("%d", {{$goName}}Flag{{.GoName}}))
887+
{{- else if eq .GoType "float64"}}
888+
cliArgs = append(cliArgs, "--{{.Name}}", fmt.Sprintf("%g", {{$goName}}Flag{{.GoName}}))
827889
{{- end}}
828890
{{- end}}
829891
{{- if $hasAuth}}
830-
// Resolve auth token: prefer flag, fall back to env var.
831-
token := {{$goName}}Token
832-
if token == "" {
833-
token = os.Getenv("{{$tokenEnv | esc}}")
834-
}
835-
if token == "" {
836-
return fmt.Errorf("auth required: set {{$tokenEnv | esc}} or pass --{{$tokenFlag | esc}}")
837-
}
838-
_ = token // passed to the entrypoint via environment
892+
cliArgs = append(cliArgs, "--{{$tokenFlag | esc}}", token)
839893
{{- end}}
894+
ctx := cmd.Context()
840895
{{- if .IsBinaryOutput}}
841-
// Binary output: detect TTY and handle appropriately.
896+
// Binary output handling
842897
fi, statErr := os.Stdout.Stat()
843898
isTTY := statErr == nil && (fi.Mode()&os.ModeCharDevice) != 0
844899
if isTTY && {{$goName}}FlagOutput == "" {
845900
return fmt.Errorf("binary output requires --output <file> or pipe")
846901
}
847-
c := exec.CommandContext(cmd.Context(), "echo", "running", "{{$toolName}}")
902+
c := exec.CommandContext(ctx, entrypoint, cliArgs...)
848903
c.Stderr = os.Stderr
849904
if {{$goName}}FlagOutput != "" {
850-
// --output provided: capture stdout and write to file.
851905
out, err := c.Output()
852906
if err != nil {
853907
return fmt.Errorf("{{$toolName}} failed: %w", err)
@@ -856,15 +910,14 @@ var {{.GoName}}Cmd = &cobra.Command{
856910
return fmt.Errorf("writing output file: %w", err)
857911
}
858912
} else {
859-
// No --output: stream directly to stdout (pipe mode).
860913
c.Stdout = os.Stdout
861914
if err := c.Run(); err != nil {
862915
return fmt.Errorf("{{$toolName}} failed: %w", err)
863916
}
864917
}
865918
{{- else}}
866919
// Execute the tool entrypoint.
867-
c := exec.CommandContext(cmd.Context(), "echo", "running", "{{$toolName}}")
920+
c := exec.CommandContext(ctx, entrypoint, cliArgs...)
868921
c.Stdout = os.Stdout
869922
c.Stderr = os.Stderr
870923
if err := c.Run(); err != nil {

0 commit comments

Comments
 (0)