builder

package
v1.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 26 Imported by: 0

README

builder

builder generates the doors of an application on top of inject: a handler declares with an annotation which door serves it (a REST route on echo, a gRPC service, an MCP tool or the event function), and the generator writes one fx module per door, a Modules() per app and the main of every app. @Middleware on a constructor wraps every method of the interface it provides in extra/middleware plugins. One command runs inject and builder in one pass; the application writes only handlers, plain services and annotations:

go generate ./...
go run ./cmd/api

Annotations

@REST: a route on echo
package createorder

// New provides the handler.
// @Provide(index=0)
func New(svc *order.Service) *Handler { return &Handler{svc: svc} }

// Create creates an order.
// @REST(method=POST, path=/orders)
func (h *Handler) Create(c echo.Context) error { ... }
package probe

// Live answers the liveness probe.
// @REST(method=GET, path=/live, server=admin)
// @REST(method=HEAD, path=/live, server=admin)
func Live(c echo.Context) error { ... }
Key Required Meaning
method yes GET, POST, PUT, PATCH, DELETE, HEAD or OPTIONS
path yes echo path (/orders/:id)
server no server name, default rest; each server is its own echo instance
app no application (binary) that hosts the route, default api
  • The handler is func(echo.Context) error, a function or a method.
  • On a method, the receiver (*T or T) must be provided by a default @Provide: the door gets it from the graph.
  • @REST is repeatable: each line is one route of the same handler.
@GRPC: a service on the gRPC server
package healthcheck

import healthpb "google.golang.org/grpc/health/grpc_health_v1"

// NewServer provides the health service.
// @Provide(index=0)
// @GRPC(register=healthpb.RegisterHealthServer)
func NewServer() *health.Server { ... }
Key Required Meaning
register yes the RegisterXServer function, written as the file writes it (pkg.RegisterXServer with pkg imported by the file, or a function of the same package)
server no server name, default grpc
app no application, default api

@GRPC goes on a constructor with exactly one @Provide(index=N) without name or group; the door calls register(srv.ServiceRegistrar(), value).

@MCP: a tool on the MCP server
package stocktool

// New provides the handler.
// @Provide(index=0)
func New(stock stock.Service) *Handler { return &Handler{stock: stock} }

// CheckStock tells whether an item can be ordered.
// @MCP
func (h *Handler) CheckStock(ctx context.Context, req *mcp.CallToolRequest, in Input) (*mcp.CallToolResult, Output, error) { ... }
Key Required Meaning
name no tool name (letters, digits, _, -, ., up to 128), default the handler name in snake case (check_stock)
server no server name, default mcp
app no application, default api
  • The handler is a go-sdk mcp.ToolHandlerFor[In, Out]: func(context.Context, *mcp.CallToolRequest, In) (*mcp.CallToolResult, Out, error). The door calls mcp.AddTool, which infers the input and output schemas from In and Out and fills the result from Out, so the handler may return a nil *mcp.CallToolResult. A method needs a provided receiver, as in @REST.
  • The tool description is the doc comment of the handler, without its leading name: Tells whether an item can be ordered.
  • The handler lives in the app layer and calls the service; the service never imports mcp.
@Function: the event handler
package ordercreated

// Handle records the order of the event.
// @Function(adapter=[cloudevents, nats], app=worker, middlewares=[recovery, logger])
func (h *Handler) Handle(ctx context.Context, in cloudevents.Event) (*cloudevents.Event, error) { ... }
Key Required Meaning
adapter yes azure (Azure Functions custom handler), cloudevents, cenats (cloudevents over NATS), kafka, kafkago (kafka on segmentio/kafka-go), lambda (AWS Lambda), nats or pubsub; or a list [a, b] of them, chosen by config
app no application, default api
middlewares no [a, b], the outermost first: ignore_errors, logger, opentelemetry, prometheus, publisher, recovery
  • The handler is a function.Handler[T]: func(context.Context, cloudevents.Event) (T, error); T is its first result. A method needs a provided receiver, as in @REST.
  • An app runs at most one @Function. The door includes functionfx.Module[T] and the adapter module under fx/modules/bootstrap/function/adapter/contrib (pubsub includes v1 and v2; boost.bootstrap.function.adapter.pubsub.version, v2 by default, turns on one).
  • The nats, cenats and pubsub (v2) adapters read with the app's default client: the *nats.Conn or *pubsub.Client of the app's unnamed @Provide, or else the one inject generates (DefaultConnModule(), DefaultClientModule() under gen/client/...), the same one the publisher and an @Inject without name get. The door wires the adapter's ModuleWithoutClient, so a function that reads from a broker and publishes to it boots with one client.
  • With a list, adapter=[pubsub, nats], config chooses the adapter: <app>.function.adapter (env <APP>_FUNCTION_ADAPTER), default the first name. The door includes only the chosen adapter module, so the others and their clients are never built; an unknown name fails the boot with NotValid naming the key, the value and the valid names. The adapters' own keys (.enabled, the pubsub version) stay as they are.
  • Each middleware is the fx module under fx/modules/bootstrap/function/middleware of the bootstrap/function/middleware of that name; the door hands them to functionfx.Module under functionfx.MiddlewaresName. One turned off in config (boost.bootstrap.function.middleware.<name>.enabled) is skipped. publisher needs the app's unnamed publisher.Driver: the door builds the @Provide constructor that returns it, and generation fails with NotFound when there is none (fx would skip the middleware silently).
@Middleware: plugins around every method of a service
package stock

// New provides the stock service.
// @Provide(index=0)
// @Middleware(log, prometheus)
func New() Service { return &memory{} }
  • Plugin names, at least one, no duplicates, the first is the outermost:
Name Plugin
log plugins/local/wrapper/log
hystrix plugins/contrib/afex/hystrix-go/v0
datadog plugins/contrib/datadog/dd-trace-go/v1
prometheus plugins/contrib/prometheus/client_golang/v1
  • It goes on a function with one @Provide (with or without name, not group) whose provided result is an exported, non-generic interface. Every method (embedded ones included) takes context.Context first and returns error, (R, error) or R, and uses only exported types.
  • builder generates a decorator of the interface and applies it with a root fx.Decorate, so every consumer in the app receives it, whatever module it lives in. Each method runs next in the wrapper that fits its results (ErrorWrapper, AnyErrorWrapper[R], AnyWrapper[R]); the service's methods stay plain.
  • The wrapper of a method is named <package>.<interface>.<method>, lowercased (stock.service.available): it is the hystrix command, the span and metric name and the config path of each plugin. Config only turns one off: boost.factory.local.extra.middleware.plugins.<plugin>.<name>.enabled (default true).
  • An app whose doors never build the constructor gets neither the decorator nor its plugins.
  • On an implementation of a set (see inject -- Default implementation), the decorator applies only when config chooses that implementation, so every implementation may carry its own @Middleware of the same interface.

App and server names match ^[a-z][a-z0-9]*$ (they become a package, a file, a function and a config path); an app is not a Go keyword, main or middleware, and a server is not function.

Generating

//go:generate go run github.com/xgodev/boost/builder/cmd/builder --boost.builder.output=gen
Key Env Default Description
boost.builder.path BOOST_BUILDER_PATH . directory scanned for the builder and inject annotations
boost.builder.output BOOST_BUILDER_OUTPUT gen output directory; must be inside the Go module
boost.builder.cmd BOOST_BUILDER_CMD cmd directory that receives <app>/main.go for every app; must be inside the Go module
cmd/<app>/main.go                    # boost.Start() and fxfact.NewApp(<app>.Modules()...).Run()
gen/
  module.go                          # inject root module
  internal/.../*_module.go           # inject modules, one per @Provide/@Invoke/@Config, at the package's path in the project
  client/<client type import path>/<name>_<type>.go # a factory client asked by @Inject (see inject)
  builder/<app>/modules.go           # func Modules() []fx.Option
  builder/<app>/<server>_module.go   # one REST, gRPC or MCP server and its routes/services/tools
  builder/<app>/function_module.go   # the @Function handler and its adapter
  builder/middleware/<package path>/<func>_middleware.go # Decorate<Func>(): the @Middleware decorator

gen/ mirrors the project: a package in internal/app/api/rest gets its module in gen/internal/app/api/rest, so apps whose packages live under internal/ (project layout) compile and gen/ reads like the project. Below gen/builder/middleware/ and gen/implementation/ the leading internal is written internal_, which Go needs there (see inject).

Modules() returns the doors sorted by name, then the @Middleware decorators the doors reach and, when the app has servers, multiserverfx.Module(), which starts all of them. Each door includes the inject modules of its handlers, so only what the app serves is built. Generated files start with // Code generated by builder; DO NOT EDIT.; every run removes the ones no longer produced, including the main of a removed app.

Settings are generated too: a package declares a struct marked @Config whose fields carry default and desc tags, and inject registers one config key per field and provides *Struct to the constructors that receive it (see inject). A door that reaches such a constructor includes the config module, so the package never imports wrapper/config. Factory clients too: @Inject(index=0, name=quote, plugins=[otel]) on a *resty.Client (redis, mongo, NATS, Pub/Sub, gocql, AWS) parameter makes inject build that client with those plugins and its own config under boost.factory.resty.clients.quote (see inject), so the app writes no factory module.

builder owns the main of every app: a cmd/<app>/main.go without that header fails the generation instead of being overwritten. Other files under cmd/ (tests next to the main, other commands) are kept.

Servers
  • rest, grpc and mcp use the shipped echo, gRPC and MCP fx modules, configured under boost.factory.echo, boost.factory.grpc.server and boost.factory.mcp.server.
  • Any other name is a separate server configured under <app>.server.<name> (api.server.admin.port): give it its own port.
  • A server name serves one kind of door: REST, gRPC or MCP.
  • Plugins are optional and come from @Provide: a []boostecho.Plugin (REST) or []server.Plugin (gRPC, grpc/v1/server; MCP, go-sdk/v1/server) provided with name=<server> reaches that server, rest, grpc and mcp included; one provided without name reaches rest, grpc or mcp too. The door includes the provider's inject module, so the generated main needs nothing else:
// Plugins tags the admin responses.
// @Provide(name=admin, index=0)
func Plugins() []boostecho.Plugin { ... }

Validation

The command fails, without touching the output directory, when:

  • method is missing or unknown, or path is missing;
  • the same method+path is declared twice in one server, a gRPC register twice, or an MCP tool name twice;
  • an MCP tool name has a character other than letters, digits, _, - and ., or more than 128;
  • the handler signature does not match the door;
  • a method handler has no @Provide of its receiver;
  • the receiver of a handler method, or a @GRPC constructor, is one of the implementations of a set (@Default): a door is built from one constructor, so it takes the chosen value through a type of its own;
  • @GRPC has no single default @Provide, or register does not resolve to an exported function;
  • an app has two @Function, or adapter is missing or unknown, or its list is empty or repeats a name;
  • middlewares is not written as [a, b], or lists an empty, unknown or repeated name;
  • a server is used by two kinds of door;
  • rest, grpc or mcp gets both a []Plugin with name=<server> and one without name: provide one of them;
  • an app or server name is invalid, or an annotation has an unknown key;
  • the handler is unexported or in package main;
  • @Middleware lists no plugin, an unknown one or one twice, is repeated, is on a struct, is not on a single @Provide, decorates a group, a type that is not an exported non-generic interface with methods, or a method that breaks the rules above, or two constructors decorate the same interface (unless they are implementations of the same set);
  • cmd/<app>/main.go exists and was not generated by builder;
  • any inject validation fails (see inject).

Library

// what cmd/builder does: gen/ under the module of src, mains under cmd
err := builder.Generate(ctx, src, "gen", "cmd") // empty cmd: no main

// the files Generate would write, change or remove, relative to src
stale, err := builder.Stale(ctx, src, "gen", "cmd")

Stale writes nothing, which lets a test of the app check that gen/ and the mains are up to date:

func TestGeneratedCodeIsUpToDate(t *testing.T) {
	stale, err := builder.Stale(context.Background(), ".", "gen", "cmd")
	require.NoError(t, err)
	assert.Empty(t, stale, "run go generate")
}

The steps underneath, for a tool of its own:

modulePath, moduleDir, _ := inject.FindModule(".")
entries, _ := builder.CollectEntries(".")
g, _ := inject.NewGraphFromEntries(ctx, entries)
spec, _ := builder.NewSpec(entries, g, builder.LoadPackages(".")) // loads the interfaces @Middleware decorates
opts := builder.Options{
	GeneratorOptions: inject.GeneratorOptions{ModulePath: modulePath, ModuleDir: moduleDir, OutputDir: outputDir},
	CmdDir:           cmdDir, // empty: no main
}
doors := builder.NewGenerator(spec, opts)
files, _ := doors.Render(ctx)    // builder/<app>/... and builder/middleware/... relative to OutputDir
mains, _ := doors.RenderCmd(ctx) // <app>/main.go relative to CmdDir
// or doors.Generate(ctx) to write both

Example

examples/simple has two apps:

  • api: REST POST /orders and GET /orders/:id on rest, GET/HEAD /live on admin (built with the X-Served-By: admin plugin of probe.Plugins), the gRPC health service on grpc and the check_stock MCP tool of stocktool on mcp; POST /orders and check_stock check stock.Service, implemented by stock/memory (@Default, with @Middleware(log, prometheus)) and stock/unlimited, chosen by builder.examples.simple.stock.implementation (BUILDER_EXAMPLES_SIMPLE_STOCK_IMPLEMENTATION); stock/memory reads its sold-out items from its @Config Options (builder.examples.simple.stock.memory.soldOut, env BUILDER_EXAMPLES_SIMPLE_STOCK_MEMORY_SOLD__OUT, default sold-out);
  • worker: the ordercreated function on the cloudevents or the nats adapter, chosen by worker.function.adapter (WORKER_FUNCTION_ADAPTER, default cloudevents), behind the recovery and logger middlewares.

cmd/api/main.go and cmd/worker/main.go are generated. Its tests call the routes, the gRPC service and the MCP tool of the generated app, check that the handler receives the stock decorator and that prometheus counted its call, boot the api with unlimited (wired without the decorator), with an unknown name (the boot fails) and with SOLD__OUT=book,pen (the env replaces the default list), run the function handler, boot the worker with each adapter (nats on an embedded server) and with an unknown one (the boot fails), and fail when gen/ or a main is stale.

Non-goals

  • A YAML spec: annotations are the only source, and server settings stay in boost config.
  • Generating OpenAPI or proto from annotations: the spec in api/ is the source, and code may come from it, never the reverse.
  • Transport-agnostic handlers: a handler is native to its door.
  • Per-route middleware and GraphQL.
  • cache and fallback in @Middleware (they need a cache manager or a fallback per method), and different middlewares per method.

Documentation

Overview

Package builder generates the doors of an application (REST routes on echo, gRPC services, MCP tools and event functions) from @REST, @GRPC, @MCP and @Function annotations, on top of the fx modules generated by inject.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CollectEntries

func CollectEntries(path string) ([]annotation.Entry, error)

CollectEntries returns the declarations under path, and in the packages they import, annotated with a door (@REST, @GRPC, @MCP, @Function) or with the inject annotations (@Provide, @Inject, @Invoke, @Default, @Config) that build their receivers, and the @Middleware of constructors.

func Generate added in v1.3.0

func Generate(ctx context.Context, src, out, cmd string) error

Generate writes the inject modules and the doors of the annotations under src to out, and the main of every app to cmd (none when cmd is empty): what cmd/builder does. It writes nothing when an annotation is not valid or the main of an app was written by hand.

func Stale added in v1.3.0

func Stale(ctx context.Context, src, out, cmd string) ([]string, error)

Stale returns the files Generate would write, change or remove under out and cmd, as slash-separated paths relative to src, sorted; none when the generated code is up to date. It writes nothing, so a test of the application can fail when gen/ or a main is stale:

stale, err := builder.Stale(ctx, ".", "gen", "cmd")

Types

type App

type App struct {
	Name     string
	Servers  []Server  // sorted by name
	Function *Function // nil when the app has no @Function
	// Middlewares are the @Middleware of the constructors the doors of the
	// app build, sorted by constructor.
	Middlewares []Middleware
}

App is one binary: its servers and, optionally, its event function.

type Function

type Function struct {
	// Adapter is the adapter of adapter=x, or the first of adapter=[x, y]:
	// the one wired when <app>.function.adapter is unset.
	Adapter string
	// Alternatives are the other adapters of adapter=[x, y], in order, that
	// <app>.function.adapter may choose instead. Empty for a single adapter.
	Alternatives []string
	Handler      Handler
	Middlewares  []string // middleware names, the outermost first
	// Providers are the inject constructors of the values the middlewares
	// are built with, like the publisher.Driver of publisher. Sorted by key.
	Providers []inject.Component
	// ClientProviders are the inject constructors of the default clients the
	// adapters read with, by client type ("<import path>.<Type>"). inject
	// builds the default client of a type no constructor provides.
	ClientProviders map[string]inject.Component
}

Function is a @Function annotation: the event handler of a function app, the adapter that feeds it and the middlewares it runs around the handler.

func (Function) Adapters added in v1.6.1

func (f Function) Adapters() []string

Adapters returns the adapter and its alternatives, in order.

type Generator

type Generator struct {
	// contains filtered or unexported fields
}

Generator writes, under OutputDir/builder/<app>, one fx module per door of the app (a REST or gRPC server, or the function) and a modules.go whose Modules() returns all of them and the decorators of the app's @Middleware, written under OutputDir/builder/middleware/<import path>, and under CmdDir/<app> the main that runs them.

func NewGenerator

func NewGenerator(spec *Spec, opts Options) *Generator

NewGenerator returns a generator for the spec.

func (*Generator) Generate

func (g *Generator) Generate(ctx context.Context) error

Generate writes the doors under OutputDir/builder and the mains under CmdDir, and removes the builder files of previous runs that are no longer produced. Nothing is written when one of them cannot be rendered.

func (*Generator) Render

func (g *Generator) Render(context.Context) (map[string][]byte, error)

Render returns the generated files keyed by their slash-separated path relative to OutputDir (builder/<app>/... and builder/middleware/...), without touching the disk.

func (*Generator) RenderCmd

func (g *Generator) RenderCmd(context.Context) (map[string][]byte, error)

RenderCmd returns the main of every app keyed by its slash-separated path relative to CmdDir (<app>/main.go). It fails when the main of an app exists and was not generated by builder: builder owns the main of every app.

type Handler

type Handler struct {
	Entry annotation.Entry
	// Receiver is how a method gets its receiver; nil for a package function.
	Receiver *Receiver
}

Handler is the annotated function or method serving a door.

type Kind

type Kind string

Kind is the protocol a server speaks.

const (
	// KindREST is an echo server serving @REST routes.
	KindREST Kind = "rest"
	// KindGRPC is a gRPC server serving @GRPC services.
	KindGRPC Kind = "grpc"
	// KindMCP is an MCP server serving @MCP tools.
	KindMCP Kind = "mcp"
)

type Middleware

type Middleware struct {
	Provider  inject.Component // the annotated constructor
	Index     int              // result index of the interface value
	Plugins   []string         // plugin names, the outermost first
	Interface *types.Named     // the provided interface
	// Implementation is the implementation set the constructor is a member
	// of (@Default): the decorator runs only when config chooses it. Nil
	// for a constructor that is the only provider of its value.
	Implementation *inject.Implementation
}

Middleware is a @Middleware annotation: the plugins run around every method of the interface its constructor provides.

type Options

type Options struct {
	inject.GeneratorOptions

	// CmdDir receives <app>/main.go for every app; it must be inside the
	// module. Empty, no main is generated.
	CmdDir string
}

Options are the options of the inject generator of the same application, whose modules the doors include, and the directory of the mains.

type PackageLoader

type PackageLoader func(paths ...string) (map[string]*types.Package, error)

PackageLoader returns the type-checked packages at the import paths, keyed by path. NewSpec uses it to read the interfaces @Middleware decorates.

func LoadPackages

func LoadPackages(dir string) PackageLoader

LoadPackages returns a PackageLoader that loads the packages with go/packages from dir, which must be inside the module that imports them.

type Receiver

type Receiver struct {
	Type     string // provided type as written in the handler's package: "*Handler" or "Handler"
	Provider inject.Component
}

Receiver is the inject component providing the receiver of a handler method.

type Route

type Route struct {
	Method  string // upper case
	Path    string
	Handler Handler
}

Route is a @REST annotation: an HTTP method and path served by a handler.

type Server

type Server struct {
	App      string
	Name     string
	Kind     Kind
	Routes   []Route   // KindREST, sorted by path then method
	Services []Service // KindGRPC, sorted by register function
	Tools    []Tool    // KindMCP, sorted by name
	// Plugins are the inject constructors of the []Plugin the server is built
	// with: the value named after the server, or the default (unnamed) value
	// for rest, grpc and mcp. Sorted by key.
	Plugins []inject.Component
	// PluginsName is the name Plugins are provided under, empty for the
	// default value and when there are no Plugins.
	PluginsName string
}

Server is one echo, gRPC or MCP server of an app, with its own config path.

type Service

type Service struct {
	Register string           // register function as written in the constructor's file
	Provider inject.Component // the annotated constructor
	Index    int              // result index of the service value
}

Service is a @GRPC annotation: the value provided by an inject constructor, registered with a generated Register<Service>Server function.

type Spec

type Spec struct {
	Apps []App // sorted by name
}

Spec is every door declared by the annotations, grouped by application.

func NewSpec

func NewSpec(entries []annotation.Entry, g *graph.Graph[inject.Component], load PackageLoader) (*Spec, error)

NewSpec builds the doors declared by the @REST, @GRPC, @MCP and @Function annotations of entries, and the @Middleware of the constructors each app builds. g is the inject graph of the same entries: it provides the receivers of handler methods and the values of @GRPC constructors. load reads the interfaces @Middleware decorates; it is only called, once, when there is a @Middleware.

It returns NotValid for malformed annotations and for handlers that are unexported, in package main or whose signature does not match their door, NotFound when the receiver of a handler method or a value a @Function middleware needs has no provider, and AlreadyExists for a duplicate route, service or tool, for a second @Function in an app, for two @Middleware that decorate the same interface and for a rest, grpc or mcp server given both a named and an unnamed []Plugin.

func (Spec) DefaultClients added in v1.6.1

func (s Spec) DefaultClients() []string

DefaultClients returns the client types ("<import path>.<Type>") the adapters of the functions read with that no inject constructor provides, sorted: inject generates their default client for the function doors.

type Tool

type Tool struct {
	Name        string
	Description string // the doc comment of the handler
	Handler     Handler
}

Tool is a @MCP annotation: an MCP tool served by a handler.

Directories

Path Synopsis
cmd
builder command
Command builder generates the inject modules, the door modules and the main of every app from the annotations of an application, in one pass.
Command builder generates the inject modules, the door modules and the main of every app from the annotations of an application, in one pass.
examples
simple
Package simple is an application whose doors come from builder annotations: app api serves REST routes on two echo servers (rest and admin), a gRPC health service and the check_stock MCP tool; app worker records the orders of order.created events.
Package simple is an application whose doors come from builder annotations: app api serves REST routes on two echo servers (rest and admin), a gRPC health service and the check_stock MCP tool; app worker records the orders of order.created events.
simple/cmd/api command
Command api runs app api: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/api.
Command api runs app api: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/api.
simple/cmd/worker command
Command worker runs app worker: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/worker.
Command worker runs app worker: the doors builder generates in github.com/xgodev/boost/builder/examples/simple/gen/builder/worker.
simple/createorder
Package createorder serves the order routes of app api.
Package createorder serves the order routes of app api.
simple/healthcheck
Package healthcheck serves grpc.health.v1.Health on the grpc server of app api.
Package healthcheck serves grpc.health.v1.Health on the grpc server of app api.
simple/order
Package order keeps the orders of the example in memory.
Package order keeps the orders of the example in memory.
simple/ordercreated
Package ordercreated records the orders of order.created events in app worker.
Package ordercreated records the orders of order.created events in app worker.
simple/probe
Package probe answers the liveness probe on the admin server of app api, apart from the public routes.
Package probe answers the liveness probe on the admin server of app api, apart from the public routes.
simple/stock
Package stock tells whether an item can be ordered.
Package stock tells whether an item can be ordered.
simple/stock/memory
Package memory keeps the stock in memory.
Package memory keeps the stock in memory.
simple/stock/remote
Package remote asks a warehouse over HTTP for the stock.
Package remote asks a warehouse over HTTP for the stock.
simple/stock/unlimited
Package unlimited is a stock without limits.
Package unlimited is a stock without limits.
simple/stocktool
Package stocktool serves the stock tools of app api over MCP.
Package stocktool serves the stock tools of app api over MCP.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL