analytics_ops 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +108 -0
- data/CONTRIBUTING.md +1 -0
- data/README.md +141 -13
- data/SECURITY.md +11 -2
- data/docs/ai-connections.md +192 -0
- data/docs/api-support-matrix.md +25 -8
- data/docs/architecture.md +78 -17
- data/docs/authentication.md +53 -3
- data/docs/bigquery.md +128 -0
- data/docs/commands.md +133 -12
- data/docs/configuration-schema-v1.json +608 -50
- data/docs/configuration.md +118 -5
- data/docs/google-client-compatibility.md +21 -1
- data/docs/governance.md +93 -0
- data/docs/health.md +82 -0
- data/docs/live-smoke-test.md +13 -10
- data/docs/rails.md +25 -9
- data/docs/reports.md +165 -3
- data/docs/safety.md +37 -5
- data/docs/troubleshooting.md +81 -7
- data/lib/analytics_ops/bigquery/definition.rb +173 -0
- data/lib/analytics_ops/bigquery.rb +4 -0
- data/lib/analytics_ops/cli/local_commands.rb +80 -0
- data/lib/analytics_ops/cli/options.rb +282 -9
- data/lib/analytics_ops/cli/presenter.rb +263 -28
- data/lib/analytics_ops/cli/presenter_csv.rb +92 -0
- data/lib/analytics_ops/cli/reporting_commands.rb +129 -0
- data/lib/analytics_ops/cli/setup_commands.rb +91 -0
- data/lib/analytics_ops/cli.rb +121 -44
- data/lib/analytics_ops/clients/admin.rb +28 -2
- data/lib/analytics_ops/clients/admin_governance.rb +95 -0
- data/lib/analytics_ops/clients/admin_governance_normalization.rb +90 -0
- data/lib/analytics_ops/clients/admin_resource_normalization.rb +61 -0
- data/lib/analytics_ops/clients/bigquery.rb +313 -0
- data/lib/analytics_ops/clients/data.rb +147 -54
- data/lib/analytics_ops/clients/data_result_normalization.rb +55 -0
- data/lib/analytics_ops/clients/error_translation.rb +13 -1
- data/lib/analytics_ops/configuration/schema.rb +175 -1
- data/lib/analytics_ops/configuration/validator.rb +45 -7
- data/lib/analytics_ops/configuration/validator_experimental.rb +136 -0
- data/lib/analytics_ops/configuration/validator_reporting.rb +93 -0
- data/lib/analytics_ops/configuration/writer.rb +112 -7
- data/lib/analytics_ops/desired_state.rb +28 -3
- data/lib/analytics_ops/errors.rb +33 -1
- data/lib/analytics_ops/funnels.rb +58 -0
- data/lib/analytics_ops/governance.rb +363 -0
- data/lib/analytics_ops/health.rb +310 -0
- data/lib/analytics_ops/mcp_server/custom_report_tool.rb +101 -0
- data/lib/analytics_ops/mcp_server/discovery_tools.rb +79 -0
- data/lib/analytics_ops/mcp_server/experimental_tools.rb +89 -0
- data/lib/analytics_ops/mcp_server/health_tools.rb +80 -0
- data/lib/analytics_ops/mcp_server/overview_tools.rb +49 -0
- data/lib/analytics_ops/mcp_server/standard_report_tool.rb +53 -0
- data/lib/analytics_ops/mcp_server.rb +402 -0
- data/lib/analytics_ops/portfolio.rb +216 -0
- data/lib/analytics_ops/rails/railtie.rb +19 -8
- data/lib/analytics_ops/redaction.rb +10 -6
- data/lib/analytics_ops/reports/csv_export.rb +91 -0
- data/lib/analytics_ops/reports/definition.rb +152 -14
- data/lib/analytics_ops/reports/metadata.rb +158 -0
- data/lib/analytics_ops/reports/period.rb +104 -0
- data/lib/analytics_ops/reports/result.rb +8 -3
- data/lib/analytics_ops/reports.rb +3 -0
- data/lib/analytics_ops/service_account.rb +321 -29
- data/lib/analytics_ops/setup.rb +31 -7
- data/lib/analytics_ops/version.rb +1 -1
- data/lib/analytics_ops/workspace.rb +222 -10
- data/lib/analytics_ops.rb +7 -2
- data/lib/generators/analytics_ops/templates/analytics_ops.yml +1 -1
- data/sig/analytics_ops.rbs +494 -9
- metadata +60 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticsOps
|
|
4
|
+
# Reopens the read-only MCP server to add overview tools.
|
|
5
|
+
class MCPServer
|
|
6
|
+
# Overview MCP tools for one profile or a configured portfolio.
|
|
7
|
+
module OverviewTools
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def define_overview_tool
|
|
11
|
+
owner = self
|
|
12
|
+
server.define_tool(
|
|
13
|
+
name: "analytics_overview",
|
|
14
|
+
title: "Show Analytics Overview",
|
|
15
|
+
description: "Use this for users, sessions, trends, acquisition, landing pages, and devices.",
|
|
16
|
+
input_schema: period_schema(profile: true),
|
|
17
|
+
output_schema: OBJECT_OUTPUT,
|
|
18
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
19
|
+
) do |profile: nil, last_days: nil, start_date: nil, end_date: nil, compare: false, server_context: nil|
|
|
20
|
+
owner.__send__(:respond, server_context) do
|
|
21
|
+
ranges = owner.__send__(:date_ranges, last_days:, start_date:, end_date:, compare:)
|
|
22
|
+
target = owner.__send__(:workspace, profile)
|
|
23
|
+
ranges ? target.overview(date_ranges: ranges) : target.overview
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def define_portfolio_tool
|
|
29
|
+
owner = self
|
|
30
|
+
server.define_tool(
|
|
31
|
+
name: "analytics_portfolio",
|
|
32
|
+
title: "Compare All Analytics Properties",
|
|
33
|
+
description: "Use this for users, sessions, and key events across every configured property.",
|
|
34
|
+
input_schema: period_schema(profile: false),
|
|
35
|
+
output_schema: OBJECT_OUTPUT,
|
|
36
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
37
|
+
) do |last_days: nil, start_date: nil, end_date: nil, compare: false, server_context: nil|
|
|
38
|
+
owner.__send__(:respond, server_context) do
|
|
39
|
+
ranges = owner.__send__(:date_ranges, last_days:, start_date:, end_date:, compare:)
|
|
40
|
+
target = owner.__send__(:portfolio)
|
|
41
|
+
ranges ? target.overview(date_ranges: ranges) : target.overview
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
include OverviewTools
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module AnalyticsOps
|
|
4
|
+
# Strictly read-only MCP bridge for ChatGPT, Codex, Claude, and other MCP clients.
|
|
5
|
+
class MCPServer
|
|
6
|
+
# Defines the built-in standard-report MCP tool and its strict input schema.
|
|
7
|
+
module StandardReportTool
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def define_standard_report_tool
|
|
11
|
+
owner = self
|
|
12
|
+
server.define_tool(
|
|
13
|
+
name: "analytics_run_report",
|
|
14
|
+
title: "Run Analytics Report",
|
|
15
|
+
description: "Use this to run one safe built-in or configured GA4 standard report for the selected profile.",
|
|
16
|
+
input_schema: standard_report_schema,
|
|
17
|
+
output_schema: OBJECT_OUTPUT,
|
|
18
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
19
|
+
) do |report:, profile: nil, last_days: nil, start_date: nil, end_date: nil, compare: false,
|
|
20
|
+
server_context: nil|
|
|
21
|
+
owner.__send__(:respond, server_context) do
|
|
22
|
+
ranges = owner.__send__(
|
|
23
|
+
:date_ranges,
|
|
24
|
+
last_days:,
|
|
25
|
+
start_date:,
|
|
26
|
+
end_date:,
|
|
27
|
+
compare:
|
|
28
|
+
)
|
|
29
|
+
target = owner.__send__(:workspace, profile)
|
|
30
|
+
ranges ? target.report(report, date_ranges: ranges) : target.report(report)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def standard_report_schema
|
|
36
|
+
{
|
|
37
|
+
type: "object",
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
required: ["report"],
|
|
40
|
+
properties: PROFILE_INPUT.fetch(:properties).merge(PERIOD_PROPERTIES).merge(
|
|
41
|
+
report: {
|
|
42
|
+
type: "string",
|
|
43
|
+
pattern: "^[a-z][a-z0-9_-]{0,63}$",
|
|
44
|
+
description: "Built-in or configured report name."
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
include StandardReportTool
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "mcp"
|
|
5
|
+
require_relative "../analytics_ops"
|
|
6
|
+
|
|
7
|
+
module AnalyticsOps
|
|
8
|
+
# Strictly read-only MCP bridge for ChatGPT, Codex, Claude, and other MCP clients.
|
|
9
|
+
class MCPServer
|
|
10
|
+
PROFILE = /\A[A-Za-z][A-Za-z0-9_]{0,63}\z/
|
|
11
|
+
CONNECTION = /\A[A-Za-z][A-Za-z0-9_-]{0,63}\z/
|
|
12
|
+
CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/
|
|
13
|
+
PROFILE_JSON_PATTERN = "^[A-Za-z][A-Za-z0-9_]{0,63}$"
|
|
14
|
+
CONNECTION_JSON_PATTERN = "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
|
|
15
|
+
READ_ONLY_ANNOTATIONS = {
|
|
16
|
+
title: "Read Google Analytics",
|
|
17
|
+
read_only_hint: true,
|
|
18
|
+
destructive_hint: false,
|
|
19
|
+
idempotent_hint: true,
|
|
20
|
+
open_world_hint: true
|
|
21
|
+
}.freeze
|
|
22
|
+
PROFILE_INPUT = {
|
|
23
|
+
type: "object",
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
properties: {
|
|
26
|
+
profile: {
|
|
27
|
+
type: "string",
|
|
28
|
+
pattern: PROFILE_JSON_PATTERN,
|
|
29
|
+
description: "Optional Analytics Ops profile. Uses the locally selected profile when omitted."
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}.freeze
|
|
33
|
+
PERIOD_PROPERTIES = {
|
|
34
|
+
last_days: {
|
|
35
|
+
type: "integer",
|
|
36
|
+
minimum: 1,
|
|
37
|
+
maximum: Reports::Period::MAX_DAYS,
|
|
38
|
+
description: "Optional number of complete days ending yesterday."
|
|
39
|
+
},
|
|
40
|
+
start_date: {
|
|
41
|
+
type: "string",
|
|
42
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
43
|
+
description: "Optional YYYY-MM-DD start date; requires end_date."
|
|
44
|
+
},
|
|
45
|
+
end_date: {
|
|
46
|
+
type: "string",
|
|
47
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}$",
|
|
48
|
+
description: "Optional YYYY-MM-DD end date; requires start_date."
|
|
49
|
+
},
|
|
50
|
+
compare: {
|
|
51
|
+
type: "boolean",
|
|
52
|
+
description: "Also include the equally long preceding period."
|
|
53
|
+
}
|
|
54
|
+
}.freeze
|
|
55
|
+
OBJECT_OUTPUT = { type: "object" }.freeze
|
|
56
|
+
ARRAY_OUTPUT = { type: "array", items: { type: "object" } }.freeze
|
|
57
|
+
STANDARD_REPORTS = (
|
|
58
|
+
Reports::Catalog.names.select { |name| Reports::Catalog.fetch(name).kind == "standard" } +
|
|
59
|
+
Reports::Catalog.aliases.keys
|
|
60
|
+
).sort.freeze
|
|
61
|
+
|
|
62
|
+
attr_reader :server
|
|
63
|
+
|
|
64
|
+
def initialize(config: "config/analytics_ops.yml", profile: nil, connection: nil,
|
|
65
|
+
store: ServiceAccount::Store.new, workspace_loader: nil, connection_loader: nil,
|
|
66
|
+
service_account_loader: nil, portfolio_loader: nil,
|
|
67
|
+
transport: :grpc, timeout: nil, logger: nil)
|
|
68
|
+
@config = config
|
|
69
|
+
@profile = profile
|
|
70
|
+
@connection = connection
|
|
71
|
+
@store = store
|
|
72
|
+
@workspace_loader = workspace_loader || method(:load_workspace)
|
|
73
|
+
@connection_loader = connection_loader || method(:load_connection)
|
|
74
|
+
@service_account_loader = service_account_loader || ServiceAccount.method(:load)
|
|
75
|
+
@portfolio_loader = portfolio_loader || method(:load_portfolio)
|
|
76
|
+
@transport = transport
|
|
77
|
+
@timeout = timeout
|
|
78
|
+
@logger = logger
|
|
79
|
+
validate_defaults!
|
|
80
|
+
@server = build_server
|
|
81
|
+
define_tools
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def start
|
|
85
|
+
::MCP::Server::Transports::StdioTransport.new(server).open
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def build_server
|
|
91
|
+
configuration = ::MCP::Configuration.new(
|
|
92
|
+
validate_tool_call_arguments: true,
|
|
93
|
+
validate_tool_call_results: true
|
|
94
|
+
)
|
|
95
|
+
::MCP::Server.new(
|
|
96
|
+
name: "analytics_ops",
|
|
97
|
+
title: "Analytics Ops",
|
|
98
|
+
version: AnalyticsOps::VERSION,
|
|
99
|
+
description: "Read Google Analytics 4 configuration and reports safely.",
|
|
100
|
+
website_url: "https://github.com/nelsonchaves/analytics_ops",
|
|
101
|
+
instructions: "Every Analytics Ops tool is strictly read-only. Use these tools to inspect GA4 properties, " \
|
|
102
|
+
"configuration drift, reports, and realtime events. No tool can create a plan, apply a change, " \
|
|
103
|
+
"or modify Google Analytics. Never claim that a tool changed GA4.",
|
|
104
|
+
configuration:
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def define_tools
|
|
109
|
+
define_local_tools
|
|
110
|
+
define_google_tools
|
|
111
|
+
define_reporting_tools
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def define_local_tools
|
|
115
|
+
owner = self
|
|
116
|
+
server.define_tool(
|
|
117
|
+
name: "analytics_list_profiles",
|
|
118
|
+
title: "List Analytics Profiles",
|
|
119
|
+
description: "Use this to list locally configured GA4 property profiles and see which one is selected.",
|
|
120
|
+
input_schema: { type: "object", additionalProperties: false },
|
|
121
|
+
output_schema: ARRAY_OUTPUT,
|
|
122
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
123
|
+
) do |server_context: nil|
|
|
124
|
+
owner.__send__(:respond, server_context) { owner.__send__(:profile_rows) }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
server.define_tool(
|
|
128
|
+
name: "analytics_list_connections",
|
|
129
|
+
title: "List Analytics Connections",
|
|
130
|
+
description: "Use this to list saved Google connection names and availability. " \
|
|
131
|
+
"Credential paths are never returned.",
|
|
132
|
+
input_schema: { type: "object", additionalProperties: false },
|
|
133
|
+
output_schema: ARRAY_OUTPUT,
|
|
134
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
135
|
+
) do |server_context: nil|
|
|
136
|
+
owner.__send__(:respond, server_context) { owner.__send__(:connection_rows) }
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def define_google_tools
|
|
141
|
+
define_properties_tool
|
|
142
|
+
define_profile_tool(
|
|
143
|
+
"analytics_doctor",
|
|
144
|
+
"Check Analytics Setup",
|
|
145
|
+
"Use this to verify credentials, API access, property access, and client compatibility.",
|
|
146
|
+
OBJECT_OUTPUT
|
|
147
|
+
) { |profile| workspace(profile).doctor }
|
|
148
|
+
define_snapshot_tool
|
|
149
|
+
define_profile_tool(
|
|
150
|
+
"analytics_audit",
|
|
151
|
+
"Audit GA4 Configuration",
|
|
152
|
+
"Use this to compare desired and remote GA4 configuration. It reports drift but cannot save or apply a plan.",
|
|
153
|
+
OBJECT_OUTPUT
|
|
154
|
+
) { |profile| workspace(profile).audit }
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def define_properties_tool
|
|
158
|
+
owner = self
|
|
159
|
+
server.define_tool(
|
|
160
|
+
name: "analytics_list_properties",
|
|
161
|
+
title: "List Accessible GA4 Properties",
|
|
162
|
+
description: "Use this to discover GA4 accounts and properties accessible through a saved Google connection.",
|
|
163
|
+
input_schema: {
|
|
164
|
+
type: "object",
|
|
165
|
+
additionalProperties: false,
|
|
166
|
+
properties: {
|
|
167
|
+
connection: {
|
|
168
|
+
type: "string",
|
|
169
|
+
pattern: CONNECTION_JSON_PATTERN,
|
|
170
|
+
description: "Optional saved connection name."
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
output_schema: ARRAY_OUTPUT,
|
|
175
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
176
|
+
) do |connection: nil, server_context: nil|
|
|
177
|
+
owner.__send__(:respond, server_context) do
|
|
178
|
+
owner.__send__(:google_connection, connection).properties
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def define_snapshot_tool
|
|
184
|
+
define_profile_tool(
|
|
185
|
+
"analytics_snapshot",
|
|
186
|
+
"Read GA4 Configuration",
|
|
187
|
+
"Use this to read the selected property's normalized GA4 configuration.",
|
|
188
|
+
OBJECT_OUTPUT
|
|
189
|
+
) do |profile|
|
|
190
|
+
snapshot = workspace(profile).snapshot
|
|
191
|
+
{ "fingerprint" => snapshot.fingerprint, "snapshot" => snapshot.to_h }
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def define_reporting_tools
|
|
196
|
+
define_overview_tool
|
|
197
|
+
define_standard_report_tool
|
|
198
|
+
define_portfolio_tool
|
|
199
|
+
define_report_discovery_tools
|
|
200
|
+
define_custom_report_tool
|
|
201
|
+
define_health_tools
|
|
202
|
+
define_experimental_read_tools
|
|
203
|
+
define_profile_tool(
|
|
204
|
+
"analytics_realtime",
|
|
205
|
+
"Read Realtime Analytics",
|
|
206
|
+
"Use this to read current GA4 event counts from the safe realtime_events recipe.",
|
|
207
|
+
OBJECT_OUTPUT
|
|
208
|
+
) { |profile| workspace(profile).realtime }
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def define_profile_tool(name, title, description, output_schema, &operation)
|
|
212
|
+
owner = self
|
|
213
|
+
server.define_tool(
|
|
214
|
+
name:,
|
|
215
|
+
title:,
|
|
216
|
+
description:,
|
|
217
|
+
input_schema: PROFILE_INPUT,
|
|
218
|
+
output_schema:,
|
|
219
|
+
annotations: READ_ONLY_ANNOTATIONS
|
|
220
|
+
) do |profile: nil, server_context: nil|
|
|
221
|
+
owner.__send__(:respond, server_context) do
|
|
222
|
+
owner.instance_exec(profile, &operation)
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def period_schema(profile:)
|
|
228
|
+
properties = profile ? PROFILE_INPUT.fetch(:properties) : {}
|
|
229
|
+
{
|
|
230
|
+
type: "object",
|
|
231
|
+
additionalProperties: false,
|
|
232
|
+
properties: properties.merge(PERIOD_PROPERTIES)
|
|
233
|
+
}
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def health_period_schema(profile:)
|
|
237
|
+
properties = profile ? PROFILE_INPUT.fetch(:properties) : {}
|
|
238
|
+
health_period = PERIOD_PROPERTIES.except(:compare)
|
|
239
|
+
{
|
|
240
|
+
type: "object",
|
|
241
|
+
additionalProperties: false,
|
|
242
|
+
properties: properties.merge(health_period)
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def respond(server_context)
|
|
247
|
+
server_context&.raise_if_cancelled!
|
|
248
|
+
payload = serializable(yield)
|
|
249
|
+
server_context&.raise_if_cancelled!
|
|
250
|
+
::MCP::Tool::Response.new(
|
|
251
|
+
[{ type: "text", text: JSON.generate(payload) }],
|
|
252
|
+
structured_content: payload
|
|
253
|
+
)
|
|
254
|
+
rescue AnalyticsOps::Error => error
|
|
255
|
+
error_response(error.class.name.split("::").last, error.message)
|
|
256
|
+
rescue StandardError => error
|
|
257
|
+
SafeLogging.write(@logger, :error, "mcp_tool_error", "type" => error.class.name)
|
|
258
|
+
error_response("InternalError", "Analytics Ops could not complete this read-only request")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def error_response(type, message)
|
|
262
|
+
payload = {
|
|
263
|
+
"error" => {
|
|
264
|
+
"type" => Redaction.message(type),
|
|
265
|
+
"message" => Redaction.message(message)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
::MCP::Tool::Response.new(
|
|
269
|
+
[{ type: "text", text: JSON.generate(payload) }],
|
|
270
|
+
structured_content: payload,
|
|
271
|
+
error: true
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def profile_rows
|
|
276
|
+
selected = @store.selection(config: @config)
|
|
277
|
+
selected_profile = selected&.fetch("profile") || profile_name(nil)
|
|
278
|
+
Configuration.load(@config).profiles.sort.map do |name, desired_state|
|
|
279
|
+
{
|
|
280
|
+
"name" => name,
|
|
281
|
+
"property_id" => desired_state.property_id,
|
|
282
|
+
"connection" => @store.profile_connection(config: @config, profile: name),
|
|
283
|
+
"selected" => selected_profile == name
|
|
284
|
+
}
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def connection_rows
|
|
289
|
+
@store.summaries
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def workspace(requested_profile)
|
|
293
|
+
profile = profile_name(requested_profile)
|
|
294
|
+
service_account = load_service_account(profile:, connection: @connection)
|
|
295
|
+
@workspace_loader.call(
|
|
296
|
+
config: @config,
|
|
297
|
+
profile:,
|
|
298
|
+
service_account:,
|
|
299
|
+
transport: @transport,
|
|
300
|
+
timeout: @timeout,
|
|
301
|
+
logger: @logger
|
|
302
|
+
)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def google_connection(requested_connection)
|
|
306
|
+
profile = profile_name(nil)
|
|
307
|
+
service_account = load_service_account(profile:, connection: requested_connection || @connection)
|
|
308
|
+
@connection_loader.call(
|
|
309
|
+
service_account:,
|
|
310
|
+
transport: @transport,
|
|
311
|
+
timeout: @timeout,
|
|
312
|
+
logger: @logger
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def load_service_account(profile:, connection:)
|
|
317
|
+
@service_account_loader.call(
|
|
318
|
+
store: @store,
|
|
319
|
+
connection:,
|
|
320
|
+
config: @config,
|
|
321
|
+
profile:
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def load_workspace(**options)
|
|
326
|
+
Workspace.load(
|
|
327
|
+
options.fetch(:config),
|
|
328
|
+
profile: options.fetch(:profile),
|
|
329
|
+
service_account: options.fetch(:service_account),
|
|
330
|
+
transport: options.fetch(:transport),
|
|
331
|
+
timeout: options[:timeout],
|
|
332
|
+
logger: options[:logger]
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def load_connection(**)
|
|
337
|
+
Connection.new(**)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def portfolio
|
|
341
|
+
@portfolio_loader.call(
|
|
342
|
+
config: @config,
|
|
343
|
+
store: @store,
|
|
344
|
+
workspace_loader: @workspace_loader,
|
|
345
|
+
service_account_loader: @service_account_loader,
|
|
346
|
+
transport: @transport,
|
|
347
|
+
timeout: @timeout,
|
|
348
|
+
logger: @logger
|
|
349
|
+
)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def load_portfolio(**)
|
|
353
|
+
Portfolio.new(**)
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def date_ranges(last_days:, start_date:, end_date:, compare:)
|
|
357
|
+
Reports::Period.resolve(last_days:, start_date:, end_date:, compare:)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def profile_name(requested)
|
|
361
|
+
selected = requested || @profile || @store.selected_profile(config: @config) || "production"
|
|
362
|
+
raise ConfigurationError, "Invalid Analytics Ops profile" unless PROFILE.match?(selected.to_s)
|
|
363
|
+
|
|
364
|
+
selected.to_s
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def validate_defaults!
|
|
368
|
+
valid_config = @config.is_a?(String) &&
|
|
369
|
+
@config.length.between?(1, 4_096) &&
|
|
370
|
+
!@config.match?(CONTROL_CHARACTERS)
|
|
371
|
+
raise ConfigurationError, "MCP configuration path is invalid" unless valid_config
|
|
372
|
+
raise ConfigurationError, "MCP profile is invalid" if @profile && !valid_profile?(@profile)
|
|
373
|
+
raise ConfigurationError, "MCP connection is invalid" if @connection && !valid_connection?(@connection)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def valid_profile?(value)
|
|
377
|
+
value.is_a?(String) && PROFILE.match?(value)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def valid_connection?(value)
|
|
381
|
+
value.is_a?(String) && CONNECTION.match?(value)
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def serializable(value)
|
|
385
|
+
case value
|
|
386
|
+
when Array
|
|
387
|
+
value.map { |item| serializable(item) }
|
|
388
|
+
when Hash
|
|
389
|
+
value.to_h { |key, item| [key.to_s, serializable(item)] }
|
|
390
|
+
else
|
|
391
|
+
value.respond_to?(:to_h) ? serializable(value.to_h) : value
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
require_relative "mcp_server/experimental_tools"
|
|
398
|
+
require_relative "mcp_server/discovery_tools"
|
|
399
|
+
require_relative "mcp_server/custom_report_tool"
|
|
400
|
+
require_relative "mcp_server/health_tools"
|
|
401
|
+
require_relative "mcp_server/overview_tools"
|
|
402
|
+
require_relative "mcp_server/standard_report_tool"
|