Skip to content
Draft
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
7 changes: 6 additions & 1 deletion server/cmd/gram/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/speakeasy-api/gram/server/internal/productfeatures"
"github.com/speakeasy-api/gram/server/internal/rag"
"github.com/speakeasy-api/gram/server/internal/ratelimit"

"github.com/speakeasy-api/gram/server/internal/about"
"github.com/speakeasy-api/gram/server/internal/agentworkflows"
Expand Down Expand Up @@ -652,10 +653,14 @@ func newStartCommand() *cli.Command {
return fmt.Errorf("failed to create mcp registry client: %w", err)
}

rateLimiter := ratelimit.New(redisClient, logger)
rateLimitConfigLoader := ratelimit.NewConfigLoader(logger, db, cache.NewRedisCacheAdapter(redisClient))

mux := goahttp.NewMuxer()
mux.Use(func(h http.Handler) http.Handler {
return otelhttp.NewHandler(h, "http", otelhttp.WithServerName("gram"))
})
mux.Use(middleware.RateLimitMiddleware(rateLimiter, rateLimitConfigLoader, logger))
mux.Use(middleware.CORSMiddleware(c.String("environment"), c.String("server-url"), chatSessionsManager))
mux.Use(middleware.NewHTTPLoggingMiddleware(logger))
mux.Use(customdomains.Middleware(logger, db, c.String("environment"), serverURL))
Expand Down Expand Up @@ -700,7 +705,7 @@ func newStartCommand() *cli.Command {
mcpMetadataService := mcpmetadata.NewService(logger, db, sessionManager, serverURL, siteURL, cache.NewRedisCacheAdapter(redisClient))
mcpmetadata.Attach(mux, mcpMetadataService)
externalmcp.Attach(mux, externalmcp.NewService(logger, tracerProvider, db, sessionManager, mcpRegistryClient))
mcp.Attach(mux, mcp.NewService(logger, tracerProvider, meterProvider, db, sessionManager, chatSessionsManager, env, posthogClient, serverURL, encryptionClient, cache.NewRedisCacheAdapter(redisClient), guardianPolicy, functionsOrchestrator, oauthService, billingTracker, billingRepo, telemSvc, productFeatures, ragService, temporalEnv), mcpMetadataService)
mcp.Attach(mux, mcp.NewService(logger, tracerProvider, meterProvider, db, sessionManager, chatSessionsManager, env, posthogClient, serverURL, encryptionClient, cache.NewRedisCacheAdapter(redisClient), guardianPolicy, functionsOrchestrator, oauthService, billingTracker, billingRepo, telemSvc, productFeatures, ragService, temporalEnv, rateLimiter), mcpMetadataService)
chat.Attach(mux, chat.NewService(logger, db, sessionManager, chatSessionsManager, openRouter, chatClient, posthogClient, telemSvc, assetStorage))
variations.Attach(mux, variations.NewService(logger, db, sessionManager))
customdomains.Attach(mux, customdomains.NewService(logger, db, sessionManager, &background.CustomDomainRegistrationClient{TemporalEnv: temporalEnv}))
Expand Down
14 changes: 14 additions & 0 deletions server/database/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ CREATE TABLE IF NOT EXISTS toolsets (
),
mcp_is_public BOOLEAN NOT NULL DEFAULT FALSE,
mcp_enabled BOOLEAN NOT NULL DEFAULT FALSE,
rate_limit_rpm integer,
tool_selection_mode TEXT NOT NULL DEFAULT 'static',
custom_domain_id uuid,

Expand Down Expand Up @@ -1477,3 +1478,16 @@ WHERE deleted IS FALSE AND status = 'pending';
-- Index for looking up invites by token (unconditional to prevent token reuse)
CREATE UNIQUE INDEX IF NOT EXISTS team_invites_token_key
ON team_invites (token);

-- Platform-level rate limit overrides for Gram operators
CREATE TABLE IF NOT EXISTS platform_rate_limits (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
attribute_type text NOT NULL,
attribute_value text NOT NULL,
requests_per_minute integer NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CONSTRAINT platform_rate_limits_attribute_type_value_key UNIQUE (attribute_type, attribute_value),
CONSTRAINT platform_rate_limits_attribute_type_check CHECK (attribute_type IN ('mcp_slug', 'project', 'organization')),
CONSTRAINT platform_rate_limits_requests_per_minute_check CHECK (requests_per_minute > 0)
);
10 changes: 10 additions & 0 deletions server/database/sqlc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,13 @@ sql:
out: "../internal/externalmcp/repo"
sql_package: "pgx/v5"
omit_unused_structs: true

- schema: schema.sql
queries: ../internal/ratelimit/queries.sql
engine: postgresql
gen:
go:
package: "repo"
out: "../internal/ratelimit/repo"
sql_package: "pgx/v5"
omit_unused_structs: true
8 changes: 8 additions & 0 deletions server/design/shared/toolset.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ var Toolset = Type("Toolset", func() {
Attribute("mcp_enabled", Boolean, "Whether the toolset is enabled for MCP")
Attribute("tool_selection_mode", String, "The mode to use for tool selection")
Attribute("custom_domain_id", String, "The ID of the custom domain to use for the toolset")
Attribute("rate_limit_rpm", Int, "Maximum requests per minute for this MCP server. When set, requests exceeding this limit receive a rate limit error.", func() {
Minimum(1)
Maximum(10000)
})
Attribute("external_oauth_server", ExternalOAuthServer, "The external OAuth server details")
Attribute("oauth_proxy_server", OAuthProxyServer, "The OAuth proxy server details")
Attribute("created_at", String, func() {
Expand Down Expand Up @@ -122,6 +126,10 @@ var ToolsetEntry = Type("ToolsetEntry", func() {
Attribute("mcp_enabled", Boolean, "Whether the toolset is enabled for MCP")
Attribute("tool_selection_mode", String, "The mode to use for tool selection")
Attribute("custom_domain_id", String, "The ID of the custom domain to use for the toolset")
Attribute("rate_limit_rpm", Int, "Maximum requests per minute for this MCP server. When set, requests exceeding this limit receive a rate limit error.", func() {
Minimum(1)
Maximum(10000)
})
Attribute("created_at", String, func() {
Description("When the toolset was created.")
Format(FormatDateTime)
Expand Down
4 changes: 4 additions & 0 deletions server/design/toolsets/design.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,10 @@ var UpdateToolsetForm = Type("UpdateToolsetForm", func() {
Attribute("mcp_is_public", Boolean, "Whether the toolset is public in MCP")
Attribute("custom_domain_id", String, "The ID of the custom domain to use for the toolset")
Attribute("tool_selection_mode", String, "The mode to use for tool selection")
Attribute("rate_limit_rpm", Int, "Maximum requests per minute for this MCP server. When set, requests exceeding this limit receive a rate limit error.", func() {
Minimum(1)
Maximum(10000)
})
security.ProjectPayload()
Required("slug")
})
Expand Down
2 changes: 1 addition & 1 deletion server/gen/http/cli/gram/cli.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/gen/http/openapi3.json

Large diffs are not rendered by default.

24 changes: 24 additions & 0 deletions server/gen/http/openapi3.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 12 additions & 1 deletion server/gen/http/toolsets/client/cli.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions server/gen/http/toolsets/client/encode_decode.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading