@getverbal/cli 0.7.0 → 0.8.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.
Files changed (249) hide show
  1. package/dist/agent-hooks/claude.d.ts +10 -0
  2. package/dist/agent-hooks/claude.d.ts.map +1 -0
  3. package/dist/agent-hooks/claude.js +218 -0
  4. package/dist/agent-hooks/claude.js.map +1 -0
  5. package/dist/agent-hooks/cli.d.ts +2 -0
  6. package/dist/agent-hooks/cli.d.ts.map +1 -0
  7. package/dist/agent-hooks/cli.js +271 -0
  8. package/dist/agent-hooks/cli.js.map +1 -0
  9. package/dist/agent-hooks/codex.d.ts +9 -0
  10. package/dist/agent-hooks/codex.d.ts.map +1 -0
  11. package/dist/agent-hooks/codex.js +349 -0
  12. package/dist/agent-hooks/codex.js.map +1 -0
  13. package/dist/agent-hooks/config.d.ts +32 -0
  14. package/dist/agent-hooks/config.d.ts.map +1 -0
  15. package/dist/agent-hooks/config.js +176 -0
  16. package/dist/agent-hooks/config.js.map +1 -0
  17. package/dist/agent-hooks/ingest.d.ts +4 -0
  18. package/dist/agent-hooks/ingest.d.ts.map +1 -0
  19. package/dist/agent-hooks/ingest.js +22 -0
  20. package/dist/agent-hooks/ingest.js.map +1 -0
  21. package/dist/agent-hooks/launchagent.d.ts +7 -0
  22. package/dist/agent-hooks/launchagent.d.ts.map +1 -0
  23. package/dist/agent-hooks/launchagent.js +51 -0
  24. package/dist/agent-hooks/launchagent.js.map +1 -0
  25. package/dist/agent-hooks/runtime-context.d.ts +20 -0
  26. package/dist/agent-hooks/runtime-context.d.ts.map +1 -0
  27. package/dist/agent-hooks/runtime-context.js +90 -0
  28. package/dist/agent-hooks/runtime-context.js.map +1 -0
  29. package/dist/agent-hooks/state.d.ts +26 -0
  30. package/dist/agent-hooks/state.d.ts.map +1 -0
  31. package/dist/agent-hooks/state.js +67 -0
  32. package/dist/agent-hooks/state.js.map +1 -0
  33. package/dist/agent-hooks/tokscale.d.ts +70 -0
  34. package/dist/agent-hooks/tokscale.d.ts.map +1 -0
  35. package/dist/agent-hooks/tokscale.js +142 -0
  36. package/dist/agent-hooks/tokscale.js.map +1 -0
  37. package/dist/agent-hooks/tool-extraction.d.ts +7 -0
  38. package/dist/agent-hooks/tool-extraction.d.ts.map +1 -0
  39. package/dist/agent-hooks/tool-extraction.js +100 -0
  40. package/dist/agent-hooks/tool-extraction.js.map +1 -0
  41. package/dist/agent-hooks/trace.d.ts +17 -0
  42. package/dist/agent-hooks/trace.d.ts.map +1 -0
  43. package/dist/agent-hooks/trace.js +25 -0
  44. package/dist/agent-hooks/trace.js.map +1 -0
  45. package/dist/auth/browser-auth.d.ts +6 -0
  46. package/dist/auth/browser-auth.d.ts.map +1 -0
  47. package/dist/auth/browser-auth.js +202 -0
  48. package/dist/auth/browser-auth.js.map +1 -0
  49. package/dist/auth/credentials.d.ts +6 -0
  50. package/dist/auth/credentials.d.ts.map +1 -0
  51. package/dist/auth/credentials.js +78 -0
  52. package/dist/auth/credentials.js.map +1 -0
  53. package/dist/cli.d.ts +3 -0
  54. package/dist/cli.d.ts.map +1 -0
  55. package/dist/cli.js +550 -148
  56. package/dist/cli.js.map +1 -0
  57. package/dist/commands/dashboard.d.ts +2 -0
  58. package/dist/commands/dashboard.d.ts.map +1 -0
  59. package/dist/commands/dashboard.js +19 -0
  60. package/dist/commands/dashboard.js.map +1 -0
  61. package/dist/commands/hooks.d.ts +2 -0
  62. package/dist/commands/hooks.d.ts.map +1 -0
  63. package/dist/commands/hooks.js +6 -0
  64. package/dist/commands/hooks.js.map +1 -0
  65. package/dist/commands/import.d.ts +2 -0
  66. package/dist/commands/import.d.ts.map +1 -0
  67. package/dist/commands/import.js +129 -0
  68. package/dist/commands/import.js.map +1 -0
  69. package/dist/commands/init.d.ts +2 -0
  70. package/dist/commands/init.d.ts.map +1 -0
  71. package/dist/commands/init.js +262 -0
  72. package/dist/commands/init.js.map +1 -0
  73. package/dist/commands/logout.d.ts +2 -0
  74. package/dist/commands/logout.d.ts.map +1 -0
  75. package/dist/commands/logout.js +17 -0
  76. package/dist/commands/logout.js.map +1 -0
  77. package/dist/commands/mcp-serve.d.ts +2 -0
  78. package/dist/commands/mcp-serve.d.ts.map +1 -0
  79. package/dist/commands/mcp-serve.js +7 -0
  80. package/dist/commands/mcp-serve.js.map +1 -0
  81. package/dist/commands/status.d.ts +2 -0
  82. package/dist/commands/status.d.ts.map +1 -0
  83. package/dist/commands/status.js +43 -0
  84. package/dist/commands/status.js.map +1 -0
  85. package/dist/commands/uninstall.d.ts +2 -0
  86. package/dist/commands/uninstall.d.ts.map +1 -0
  87. package/dist/commands/uninstall.js +43 -0
  88. package/dist/commands/uninstall.js.map +1 -0
  89. package/dist/commands/update.d.ts +2 -0
  90. package/dist/commands/update.d.ts.map +1 -0
  91. package/dist/commands/update.js +58 -0
  92. package/dist/commands/update.js.map +1 -0
  93. package/dist/configure/claude-code.d.ts +7 -0
  94. package/dist/configure/claude-code.d.ts.map +1 -0
  95. package/dist/configure/claude-code.js +11 -0
  96. package/dist/configure/claude-code.js.map +1 -0
  97. package/dist/configure/claude-desktop.d.ts +8 -0
  98. package/dist/configure/claude-desktop.d.ts.map +1 -0
  99. package/dist/configure/claude-desktop.js +28 -0
  100. package/dist/configure/claude-desktop.js.map +1 -0
  101. package/dist/configure/codex.d.ts +7 -0
  102. package/dist/configure/codex.d.ts.map +1 -0
  103. package/dist/configure/codex.js +12 -0
  104. package/dist/configure/codex.js.map +1 -0
  105. package/dist/configure/cursor.d.ts +7 -0
  106. package/dist/configure/cursor.d.ts.map +1 -0
  107. package/dist/configure/cursor.js +12 -0
  108. package/dist/configure/cursor.js.map +1 -0
  109. package/dist/configure/index.d.ts +34 -0
  110. package/dist/configure/index.d.ts.map +1 -0
  111. package/dist/configure/index.js +153 -0
  112. package/dist/configure/index.js.map +1 -0
  113. package/dist/detect/claude-code.d.ts +3 -0
  114. package/dist/detect/claude-code.d.ts.map +1 -0
  115. package/dist/detect/claude-code.js +82 -0
  116. package/dist/detect/claude-code.js.map +1 -0
  117. package/dist/detect/claude-desktop.d.ts +3 -0
  118. package/dist/detect/claude-desktop.d.ts.map +1 -0
  119. package/dist/detect/claude-desktop.js +89 -0
  120. package/dist/detect/claude-desktop.js.map +1 -0
  121. package/dist/detect/codex.d.ts +3 -0
  122. package/dist/detect/codex.d.ts.map +1 -0
  123. package/dist/detect/codex.js +64 -0
  124. package/dist/detect/codex.js.map +1 -0
  125. package/dist/detect/cursor.d.ts +3 -0
  126. package/dist/detect/cursor.d.ts.map +1 -0
  127. package/dist/detect/cursor.js +81 -0
  128. package/dist/detect/cursor.js.map +1 -0
  129. package/dist/detect/index.d.ts +3 -0
  130. package/dist/detect/index.d.ts.map +1 -0
  131. package/dist/detect/index.js +28 -0
  132. package/dist/detect/index.js.map +1 -0
  133. package/dist/import/file-upload.d.ts +10 -0
  134. package/dist/import/file-upload.d.ts.map +1 -0
  135. package/dist/import/file-upload.js +37 -0
  136. package/dist/import/file-upload.js.map +1 -0
  137. package/dist/import/index.d.ts +11 -0
  138. package/dist/import/index.d.ts.map +1 -0
  139. package/dist/import/index.js +51 -0
  140. package/dist/import/index.js.map +1 -0
  141. package/dist/mcp/exports.d.ts +13 -0
  142. package/dist/mcp/exports.d.ts.map +1 -0
  143. package/dist/mcp/exports.js +13 -0
  144. package/dist/mcp/exports.js.map +1 -0
  145. package/dist/mcp/git-context.d.ts +17 -0
  146. package/dist/mcp/git-context.d.ts.map +1 -0
  147. package/dist/mcp/git-context.js +72 -0
  148. package/dist/mcp/git-context.js.map +1 -0
  149. package/dist/mcp/hooks/anthropic.d.ts +31 -0
  150. package/dist/mcp/hooks/anthropic.d.ts.map +1 -0
  151. package/dist/mcp/hooks/anthropic.js +137 -0
  152. package/dist/mcp/hooks/anthropic.js.map +1 -0
  153. package/dist/mcp/hooks/google.d.ts +53 -0
  154. package/dist/mcp/hooks/google.d.ts.map +1 -0
  155. package/dist/mcp/hooks/google.js +161 -0
  156. package/dist/mcp/hooks/google.js.map +1 -0
  157. package/dist/mcp/hooks/index.d.ts +9 -0
  158. package/dist/mcp/hooks/index.d.ts.map +1 -0
  159. package/dist/mcp/hooks/index.js +7 -0
  160. package/dist/mcp/hooks/index.js.map +1 -0
  161. package/dist/mcp/hooks/openai.d.ts +59 -0
  162. package/dist/mcp/hooks/openai.d.ts.map +1 -0
  163. package/dist/mcp/hooks/openai.js +158 -0
  164. package/dist/mcp/hooks/openai.js.map +1 -0
  165. package/dist/mcp/hooks/types.d.ts +8 -0
  166. package/dist/mcp/hooks/types.d.ts.map +1 -0
  167. package/dist/mcp/hooks/types.js +5 -0
  168. package/dist/mcp/hooks/types.js.map +1 -0
  169. package/dist/mcp/ingestor.d.ts +23 -0
  170. package/dist/mcp/ingestor.d.ts.map +1 -0
  171. package/dist/mcp/ingestor.js +310 -0
  172. package/dist/mcp/ingestor.js.map +1 -0
  173. package/dist/mcp/pricing.d.ts +19 -0
  174. package/dist/mcp/pricing.d.ts.map +1 -0
  175. package/dist/mcp/pricing.js +130 -0
  176. package/dist/mcp/pricing.js.map +1 -0
  177. package/dist/mcp/server.d.ts +10 -0
  178. package/dist/mcp/server.d.ts.map +1 -0
  179. package/dist/mcp/server.js +689 -0
  180. package/dist/mcp/server.js.map +1 -0
  181. package/dist/mcp/session-tracker.d.ts +52 -0
  182. package/dist/mcp/session-tracker.d.ts.map +1 -0
  183. package/dist/mcp/session-tracker.js +186 -0
  184. package/dist/mcp/session-tracker.js.map +1 -0
  185. package/dist/mcp/tools/analyze-spending-trend.d.ts +10 -0
  186. package/dist/mcp/tools/analyze-spending-trend.d.ts.map +1 -0
  187. package/dist/mcp/tools/analyze-spending-trend.js +126 -0
  188. package/dist/mcp/tools/analyze-spending-trend.js.map +1 -0
  189. package/dist/mcp/tools/get-budget-status.d.ts +9 -0
  190. package/dist/mcp/tools/get-budget-status.d.ts.map +1 -0
  191. package/dist/mcp/tools/get-budget-status.js +59 -0
  192. package/dist/mcp/tools/get-budget-status.js.map +1 -0
  193. package/dist/mcp/tools/get-cost-breakdown.d.ts +10 -0
  194. package/dist/mcp/tools/get-cost-breakdown.d.ts.map +1 -0
  195. package/dist/mcp/tools/get-cost-breakdown.js +52 -0
  196. package/dist/mcp/tools/get-cost-breakdown.js.map +1 -0
  197. package/dist/mcp/tools/get-model-efficiency.d.ts +9 -0
  198. package/dist/mcp/tools/get-model-efficiency.d.ts.map +1 -0
  199. package/dist/mcp/tools/get-model-efficiency.js +137 -0
  200. package/dist/mcp/tools/get-model-efficiency.js.map +1 -0
  201. package/dist/mcp/tools/get-recent-prompts.d.ts +13 -0
  202. package/dist/mcp/tools/get-recent-prompts.d.ts.map +1 -0
  203. package/dist/mcp/tools/get-recent-prompts.js +56 -0
  204. package/dist/mcp/tools/get-recent-prompts.js.map +1 -0
  205. package/dist/mcp/tools/get-roi-metrics.d.ts +9 -0
  206. package/dist/mcp/tools/get-roi-metrics.d.ts.map +1 -0
  207. package/dist/mcp/tools/get-roi-metrics.js +84 -0
  208. package/dist/mcp/tools/get-roi-metrics.js.map +1 -0
  209. package/dist/mcp/tools/get-usage-summary.d.ts +10 -0
  210. package/dist/mcp/tools/get-usage-summary.d.ts.map +1 -0
  211. package/dist/mcp/tools/get-usage-summary.js +47 -0
  212. package/dist/mcp/tools/get-usage-summary.js.map +1 -0
  213. package/dist/mcp/tools/index.d.ts +221 -0
  214. package/dist/mcp/tools/index.d.ts.map +1 -0
  215. package/dist/mcp/tools/index.js +161 -0
  216. package/dist/mcp/tools/index.js.map +1 -0
  217. package/dist/mcp/tools/list-projects.d.ts +6 -0
  218. package/dist/mcp/tools/list-projects.d.ts.map +1 -0
  219. package/dist/mcp/tools/list-projects.js +43 -0
  220. package/dist/mcp/tools/list-projects.js.map +1 -0
  221. package/dist/mcp/tools/optimize-prompt.d.ts +44 -0
  222. package/dist/mcp/tools/optimize-prompt.d.ts.map +1 -0
  223. package/dist/mcp/tools/optimize-prompt.js +95 -0
  224. package/dist/mcp/tools/optimize-prompt.js.map +1 -0
  225. package/dist/mcp/types.d.ts +118 -0
  226. package/dist/mcp/types.d.ts.map +1 -0
  227. package/dist/mcp/types.js +5 -0
  228. package/dist/mcp/types.js.map +1 -0
  229. package/dist/types.d.ts +18 -0
  230. package/dist/types.d.ts.map +1 -0
  231. package/dist/types.js +2 -0
  232. package/dist/types.js.map +1 -0
  233. package/dist/update-check/check.d.ts +6 -0
  234. package/dist/update-check/check.d.ts.map +1 -0
  235. package/dist/update-check/check.js +64 -0
  236. package/dist/update-check/check.js.map +1 -0
  237. package/dist/update-check/notify.d.ts +6 -0
  238. package/dist/update-check/notify.d.ts.map +1 -0
  239. package/dist/update-check/notify.js +40 -0
  240. package/dist/update-check/notify.js.map +1 -0
  241. package/dist/verify.d.ts +7 -0
  242. package/dist/verify.d.ts.map +1 -0
  243. package/dist/verify.js +40 -0
  244. package/dist/verify.js.map +1 -0
  245. package/dist/version.d.ts +9 -0
  246. package/dist/version.d.ts.map +1 -0
  247. package/dist/version.js +21 -0
  248. package/dist/version.js.map +1 -0
  249. package/package.json +1 -1
@@ -0,0 +1,689 @@
1
+ /**
2
+ * Verbal MCP Server with native SDK hooks
3
+ *
4
+ * This MCP server provides tools for logging LLM usage to Verbal and can be used
5
+ * with the hook adapters for transparent SDK integration.
6
+ *
7
+ * Based on the MCP Ingestion spec with native SDK hooks support.
8
+ */
9
+ import os from 'node:os';
10
+ import path from 'node:path';
11
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
14
+ import { VerbalIngestor, attachExitHooks } from './ingestor.js';
15
+ import { getRoiMetrics, analyzeSpendingTrend, getModelEfficiency, optimizePromptTool, MCP_TOOLS } from './tools/index.js';
16
+ import { syncFromTokscale } from '../agent-hooks/tokscale.js';
17
+ import { sessionTracker } from './session-tracker.js';
18
+ import { VERSION } from '../version.js';
19
+ // Load configuration from environment
20
+ const config = {
21
+ apiKey: process.env.VERBAL_API_KEY ?? '',
22
+ endpoint: process.env.VERBAL_INGEST_URL ?? '',
23
+ hmacSecret: process.env.VERBAL_HMAC_SECRET,
24
+ orgId: process.env.VERBAL_ORG_ID,
25
+ defaultProject: process.env.VERBAL_DEFAULT_PROJECT,
26
+ defaultProvider: process.env.VERBAL_DEFAULT_PROVIDER,
27
+ defaultModel: process.env.VERBAL_DEFAULT_MODEL,
28
+ source: process.env.VERBAL_SOURCE ?? 'mcp',
29
+ batchSize: Number(process.env.VERBAL_BATCH_SIZE ?? '50'),
30
+ flushIntervalMs: Number(process.env.VERBAL_FLUSH_INTERVAL_MS ?? '5000'),
31
+ httpTimeoutMs: Number(process.env.VERBAL_HTTP_TIMEOUT_MS ?? '5000'),
32
+ gitContextEnabled: process.env.VERBAL_GIT_CONTEXT_ENABLED !== 'false',
33
+ costCalcEnabled: process.env.VERBAL_COST_CALC_ENABLED !== 'false',
34
+ maxRetries: Number(process.env.VERBAL_MAX_RETRIES ?? '3'),
35
+ retryBaseDelayMs: Number(process.env.VERBAL_RETRY_BASE_DELAY_MS ?? '200'),
36
+ deadLetterPath: process.env.VERBAL_DEAD_LETTER_PATH,
37
+ };
38
+ // Initialize ingestor
39
+ const ingestor = new VerbalIngestor(config);
40
+ attachExitHooks(ingestor);
41
+ // Create MCP server
42
+ const server = new Server({
43
+ name: 'verbal-mcp',
44
+ version: VERSION,
45
+ }, {
46
+ capabilities: {
47
+ tools: {},
48
+ },
49
+ });
50
+ // Helper functions
51
+ function ensureString(value) {
52
+ if (typeof value === 'string' && value.trim().length > 0)
53
+ return value;
54
+ return undefined;
55
+ }
56
+ function ensureNumber(value) {
57
+ if (typeof value === 'number' && Number.isFinite(value))
58
+ return value;
59
+ if (typeof value === 'string') {
60
+ const parsed = Number(value);
61
+ if (Number.isFinite(parsed))
62
+ return parsed;
63
+ }
64
+ return undefined;
65
+ }
66
+ function ensureStringArray(value) {
67
+ if (!Array.isArray(value))
68
+ return undefined;
69
+ const filtered = value.filter((item) => typeof item === 'string' && item.trim().length > 0);
70
+ return filtered.length > 0 ? filtered : undefined;
71
+ }
72
+ // ListTools handler — includes ingestion tools + Phase 2 query tools from MCP_TOOLS
73
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
74
+ return {
75
+ tools: [
76
+ // Phase 2 query tools (get_roi_metrics, analyze_spending_trend, get_model_efficiency, etc.)
77
+ ...MCP_TOOLS,
78
+ // Ingestion tools
79
+ {
80
+ name: 'log_prompt',
81
+ description: 'Log a prompt (and optional response/usage metadata) to Verbal.',
82
+ inputSchema: {
83
+ type: 'object',
84
+ required: ['prompt'],
85
+ properties: {
86
+ prompt: { type: 'string', description: 'The prompt text.' },
87
+ response: { type: 'string', description: 'Optional model response.' },
88
+ provider: {
89
+ type: 'string',
90
+ description: 'Provider name (e.g., OpenAI, Anthropic).',
91
+ },
92
+ model: { type: 'string', description: 'Model identifier.' },
93
+ tokens_in: { type: 'number', description: 'Input tokens.' },
94
+ tokens_out: { type: 'number', description: 'Output tokens.' },
95
+ cache_read_tokens: { type: 'number', description: 'Cache read tokens.' },
96
+ cache_write_tokens: { type: 'number', description: 'Cache write tokens.' },
97
+ cost: { type: 'number', description: 'Cost in currency units.' },
98
+ currency: {
99
+ type: 'string',
100
+ description: 'Currency code (e.g., USD).',
101
+ },
102
+ user: {
103
+ type: 'string',
104
+ description: 'End user or operator identifier.',
105
+ },
106
+ project: {
107
+ type: 'string',
108
+ description: 'Project or cost center identifier.',
109
+ },
110
+ org_id: { type: 'string', description: 'Organization identifier.' },
111
+ prompt_name: {
112
+ type: 'string',
113
+ description: 'Human-friendly prompt name.',
114
+ },
115
+ prompt_version: { type: 'string', description: 'Prompt version.' },
116
+ session_id: { type: 'string', description: 'Session identifier.' },
117
+ request_id: { type: 'string', description: 'Upstream request id.' },
118
+ response_id: {
119
+ type: 'string',
120
+ description: 'Upstream response id.',
121
+ },
122
+ timestamp: { type: 'string', description: 'ISO 8601 timestamp.' },
123
+ tags: {
124
+ type: 'array',
125
+ items: { type: 'string' },
126
+ description: 'Tags for grouping/analysis.',
127
+ },
128
+ metadata: {
129
+ type: 'object',
130
+ description: 'Additional structured metadata.',
131
+ },
132
+ conversation_title: {
133
+ type: 'string',
134
+ description: 'Conversation title for JSON export format.',
135
+ },
136
+ conversation_id: {
137
+ type: 'string',
138
+ description: 'Verbal conversation ID to link this event to.',
139
+ },
140
+ source: { type: 'string', description: 'Capture source identifier.' },
141
+ client: { type: 'string', description: 'Capture client identifier.' },
142
+ },
143
+ },
144
+ },
145
+ {
146
+ name: 'log_usage',
147
+ description: 'Log usage-only events when prompt content is unavailable or excluded.',
148
+ inputSchema: {
149
+ type: 'object',
150
+ properties: {
151
+ provider: {
152
+ type: 'string',
153
+ description: 'Provider name (e.g., OpenAI, Anthropic).',
154
+ },
155
+ model: { type: 'string', description: 'Model identifier.' },
156
+ tokens_in: { type: 'number', description: 'Input tokens.' },
157
+ tokens_out: { type: 'number', description: 'Output tokens.' },
158
+ cache_read_tokens: { type: 'number', description: 'Cache read tokens.' },
159
+ cache_write_tokens: { type: 'number', description: 'Cache write tokens.' },
160
+ cost: { type: 'number', description: 'Cost in currency units.' },
161
+ currency: {
162
+ type: 'string',
163
+ description: 'Currency code (e.g., USD).',
164
+ },
165
+ user: {
166
+ type: 'string',
167
+ description: 'End user or operator identifier.',
168
+ },
169
+ project: {
170
+ type: 'string',
171
+ description: 'Project or cost center identifier.',
172
+ },
173
+ org_id: { type: 'string', description: 'Organization identifier.' },
174
+ session_id: { type: 'string', description: 'Session identifier.' },
175
+ request_id: { type: 'string', description: 'Upstream request id.' },
176
+ timestamp: { type: 'string', description: 'ISO 8601 timestamp.' },
177
+ tags: {
178
+ type: 'array',
179
+ items: { type: 'string' },
180
+ description: 'Tags for grouping/analysis.',
181
+ },
182
+ metadata: {
183
+ type: 'object',
184
+ description: 'Additional structured metadata.',
185
+ },
186
+ conversation_id: {
187
+ type: 'string',
188
+ description: 'Verbal conversation ID to link this event to.',
189
+ },
190
+ source: { type: 'string', description: 'Capture source identifier.' },
191
+ client: { type: 'string', description: 'Capture client identifier.' },
192
+ },
193
+ },
194
+ },
195
+ {
196
+ name: 'log_events_bulk',
197
+ description: 'Log multiple prompt/usage events in a single request.',
198
+ inputSchema: {
199
+ type: 'object',
200
+ required: ['events'],
201
+ properties: {
202
+ events: {
203
+ type: 'array',
204
+ description: 'Array of prompt/usage events.',
205
+ items: {
206
+ type: 'object',
207
+ properties: {
208
+ event_type: {
209
+ type: 'string',
210
+ description: 'prompt or usage. If omitted, inferred.',
211
+ },
212
+ prompt: { type: 'string', description: 'The prompt text.' },
213
+ response: {
214
+ type: 'string',
215
+ description: 'Optional model response.',
216
+ },
217
+ provider: {
218
+ type: 'string',
219
+ description: 'Provider name (e.g., OpenAI, Anthropic).',
220
+ },
221
+ model: { type: 'string', description: 'Model identifier.' },
222
+ tokens_in: { type: 'number', description: 'Input tokens.' },
223
+ tokens_out: { type: 'number', description: 'Output tokens.' },
224
+ cost: {
225
+ type: 'number',
226
+ description: 'Cost in currency units.',
227
+ },
228
+ currency: {
229
+ type: 'string',
230
+ description: 'Currency code (e.g., USD).',
231
+ },
232
+ user: {
233
+ type: 'string',
234
+ description: 'End user or operator identifier.',
235
+ },
236
+ project: {
237
+ type: 'string',
238
+ description: 'Project or cost center identifier.',
239
+ },
240
+ org_id: {
241
+ type: 'string',
242
+ description: 'Organization identifier.',
243
+ },
244
+ tags: {
245
+ type: 'array',
246
+ items: { type: 'string' },
247
+ description: 'Tags for grouping/analysis.',
248
+ },
249
+ metadata: {
250
+ type: 'object',
251
+ description: 'Additional structured metadata.',
252
+ },
253
+ cache_read_tokens: { type: 'number', description: 'Cache read tokens.' },
254
+ cache_write_tokens: { type: 'number', description: 'Cache write tokens.' },
255
+ prompt_name: { type: 'string', description: 'Prompt name.' },
256
+ prompt_version: { type: 'string', description: 'Prompt version.' },
257
+ session_id: { type: 'string', description: 'Session identifier.' },
258
+ request_id: { type: 'string', description: 'Request identifier.' },
259
+ response_id: { type: 'string', description: 'Response identifier.' },
260
+ timestamp: { type: 'string', description: 'ISO timestamp.' },
261
+ conversation_id: { type: 'string', description: 'Verbal conversation identifier.' },
262
+ conversation_title: { type: 'string', description: 'Conversation title.' },
263
+ source: { type: 'string', description: 'Capture source.' },
264
+ client: { type: 'string', description: 'Capture client.' },
265
+ },
266
+ },
267
+ },
268
+ },
269
+ },
270
+ },
271
+ {
272
+ name: 'sync_tokscale',
273
+ description: 'Sync usage data from local AI tool sessions via tokscale. Scans Claude Code, Codex, Cursor, Gemini, and other supported platforms.',
274
+ inputSchema: {
275
+ type: 'object',
276
+ properties: {
277
+ since: {
278
+ type: 'string',
279
+ description: 'Start date (YYYY-MM-DD). Defaults to last sync date.',
280
+ },
281
+ until: {
282
+ type: 'string',
283
+ description: 'End date (YYYY-MM-DD). Defaults to today.',
284
+ },
285
+ platforms: {
286
+ type: 'array',
287
+ items: { type: 'string' },
288
+ description: 'Platforms to sync (e.g. claude, codex, cursor). Defaults to all.',
289
+ },
290
+ project: {
291
+ type: 'string',
292
+ description: 'Project identifier to tag events with.',
293
+ },
294
+ },
295
+ },
296
+ },
297
+ {
298
+ name: 'submit_session_review',
299
+ description: 'Submit a review for a completed AI session. Rate usefulness and estimate time saved.',
300
+ inputSchema: {
301
+ type: 'object',
302
+ required: ['session_id', 'usefulness_rating', 'effectiveness_rating', 'time_saved_bucket'],
303
+ properties: {
304
+ session_id: {
305
+ type: 'string',
306
+ description: 'The session identifier to review.',
307
+ },
308
+ usefulness_rating: {
309
+ type: 'integer',
310
+ minimum: 1,
311
+ maximum: 5,
312
+ description: 'How useful was the AI assistance? (1=not useful, 5=extremely useful)',
313
+ },
314
+ effectiveness_rating: {
315
+ type: 'integer',
316
+ minimum: 1,
317
+ maximum: 5,
318
+ description: 'How effective were the AI responses? (1=poor, 5=excellent)',
319
+ },
320
+ time_saved_bucket: {
321
+ type: 'string',
322
+ enum: ['time_lost', 'none', '0_5min', '6_15min', '16_30min', '31_60min', '60plus_min'],
323
+ description: 'Estimated time saved (or lost) compared to working without AI.',
324
+ },
325
+ improvement_text: {
326
+ type: 'string',
327
+ description: 'Optional feedback on how the AI could have been more helpful.',
328
+ },
329
+ },
330
+ },
331
+ },
332
+ {
333
+ name: 'get_pending_session_reviews',
334
+ description: 'Get sessions awaiting review.',
335
+ inputSchema: {
336
+ type: 'object',
337
+ properties: {},
338
+ },
339
+ },
340
+ ],
341
+ };
342
+ });
343
+ // Tool handlers
344
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
345
+ const name = request.params.name;
346
+ const args = (request.params.arguments ?? {});
347
+ if (name === 'log_prompt') {
348
+ const prompt = ensureString(args.prompt);
349
+ if (!prompt) {
350
+ throw new Error('log_prompt requires a non-empty prompt string');
351
+ }
352
+ const event = {
353
+ event_type: 'prompt',
354
+ prompt,
355
+ response: ensureString(args.response),
356
+ provider: ensureString(args.provider),
357
+ model: ensureString(args.model),
358
+ tokens_in: ensureNumber(args.tokens_in),
359
+ tokens_out: ensureNumber(args.tokens_out),
360
+ cache_read_tokens: ensureNumber(args.cache_read_tokens),
361
+ cache_write_tokens: ensureNumber(args.cache_write_tokens),
362
+ cost: ensureNumber(args.cost),
363
+ currency: ensureString(args.currency),
364
+ user: ensureString(args.user),
365
+ project: ensureString(args.project),
366
+ org_id: ensureString(args.org_id),
367
+ prompt_name: ensureString(args.prompt_name),
368
+ prompt_version: ensureString(args.prompt_version),
369
+ session_id: ensureString(args.session_id),
370
+ request_id: ensureString(args.request_id),
371
+ response_id: ensureString(args.response_id),
372
+ timestamp: ensureString(args.timestamp),
373
+ tags: ensureStringArray(args.tags),
374
+ metadata: typeof args.metadata === 'object' && args.metadata !== null
375
+ ? args.metadata
376
+ : undefined,
377
+ conversation_id: ensureString(args.conversation_id),
378
+ conversation_title: ensureString(args.conversation_title),
379
+ source: ensureString(args.source),
380
+ client: ensureString(args.client),
381
+ };
382
+ await ingestor.track(event);
383
+ return {
384
+ content: [
385
+ {
386
+ type: 'text',
387
+ text: `Logged prompt event ${event.event_id}`,
388
+ },
389
+ ],
390
+ };
391
+ }
392
+ if (name === 'log_usage') {
393
+ const tokensIn = ensureNumber(args.tokens_in);
394
+ const tokensOut = ensureNumber(args.tokens_out);
395
+ const cost = ensureNumber(args.cost);
396
+ if (tokensIn === undefined && tokensOut === undefined && cost === undefined) {
397
+ throw new Error('log_usage requires tokens_in, tokens_out, or cost');
398
+ }
399
+ const event = {
400
+ event_type: 'usage',
401
+ provider: ensureString(args.provider),
402
+ model: ensureString(args.model),
403
+ tokens_in: tokensIn,
404
+ tokens_out: tokensOut,
405
+ cache_read_tokens: ensureNumber(args.cache_read_tokens),
406
+ cache_write_tokens: ensureNumber(args.cache_write_tokens),
407
+ cost,
408
+ currency: ensureString(args.currency),
409
+ user: ensureString(args.user),
410
+ project: ensureString(args.project),
411
+ org_id: ensureString(args.org_id),
412
+ session_id: ensureString(args.session_id),
413
+ request_id: ensureString(args.request_id),
414
+ timestamp: ensureString(args.timestamp),
415
+ tags: ensureStringArray(args.tags),
416
+ metadata: typeof args.metadata === 'object' && args.metadata !== null
417
+ ? args.metadata
418
+ : undefined,
419
+ conversation_id: ensureString(args.conversation_id),
420
+ source: ensureString(args.source),
421
+ client: ensureString(args.client),
422
+ };
423
+ await ingestor.track(event);
424
+ return {
425
+ content: [
426
+ {
427
+ type: 'text',
428
+ text: `Logged usage event ${event.event_id}`,
429
+ },
430
+ ],
431
+ };
432
+ }
433
+ if (name === 'log_events_bulk') {
434
+ const eventsArg = args.events;
435
+ if (!Array.isArray(eventsArg)) {
436
+ throw new Error('log_events_bulk requires an events array');
437
+ }
438
+ let successCount = 0;
439
+ let errorCount = 0;
440
+ for (const eventData of eventsArg) {
441
+ if (typeof eventData !== 'object' ||
442
+ eventData === null ||
443
+ Array.isArray(eventData)) {
444
+ errorCount++;
445
+ continue;
446
+ }
447
+ const eventArgs = eventData;
448
+ const eventType = ensureString(eventArgs.event_type);
449
+ // Determine event type
450
+ const isPromptEvent = eventType === 'prompt' || ensureString(eventArgs.prompt);
451
+ const event = {
452
+ event_type: isPromptEvent ? 'prompt' : 'usage',
453
+ prompt: ensureString(eventArgs.prompt),
454
+ response: ensureString(eventArgs.response),
455
+ provider: ensureString(eventArgs.provider),
456
+ model: ensureString(eventArgs.model),
457
+ tokens_in: ensureNumber(eventArgs.tokens_in),
458
+ tokens_out: ensureNumber(eventArgs.tokens_out),
459
+ cost: ensureNumber(eventArgs.cost),
460
+ currency: ensureString(eventArgs.currency),
461
+ user: ensureString(eventArgs.user),
462
+ project: ensureString(eventArgs.project),
463
+ org_id: ensureString(eventArgs.org_id),
464
+ tags: ensureStringArray(eventArgs.tags),
465
+ cache_read_tokens: ensureNumber(eventArgs.cache_read_tokens),
466
+ cache_write_tokens: ensureNumber(eventArgs.cache_write_tokens),
467
+ prompt_name: ensureString(eventArgs.prompt_name),
468
+ prompt_version: ensureString(eventArgs.prompt_version),
469
+ session_id: ensureString(eventArgs.session_id),
470
+ request_id: ensureString(eventArgs.request_id),
471
+ response_id: ensureString(eventArgs.response_id),
472
+ timestamp: ensureString(eventArgs.timestamp),
473
+ conversation_id: ensureString(eventArgs.conversation_id),
474
+ conversation_title: ensureString(eventArgs.conversation_title),
475
+ source: ensureString(eventArgs.source),
476
+ client: ensureString(eventArgs.client),
477
+ metadata: typeof eventArgs.metadata === 'object' && eventArgs.metadata !== null
478
+ ? eventArgs.metadata
479
+ : undefined,
480
+ };
481
+ try {
482
+ await ingestor.track(event);
483
+ successCount++;
484
+ }
485
+ catch (error) {
486
+ errorCount++;
487
+ console.error('[MCP] Failed to track event:', error);
488
+ }
489
+ }
490
+ return {
491
+ content: [
492
+ {
493
+ type: 'text',
494
+ text: `Bulk ingest complete. sent=${successCount}, failed=${errorCount}`,
495
+ },
496
+ ],
497
+ };
498
+ }
499
+ // Phase 2 Tools - ROI Metrics
500
+ if (name === 'get_roi_metrics') {
501
+ const apiKey = process.env.VERBAL_API_KEY || '';
502
+ const apiUrl = process.env.VERBAL_API_URL || 'http://localhost:3000';
503
+ if (!apiKey) {
504
+ throw new Error('VERBAL_API_KEY environment variable not set');
505
+ }
506
+ const timeframe = args.timeframe || '30d';
507
+ const result = await getRoiMetrics(apiKey, apiUrl, { timeframe: timeframe });
508
+ return {
509
+ content: [
510
+ {
511
+ type: 'text',
512
+ text: result,
513
+ },
514
+ ],
515
+ };
516
+ }
517
+ if (name === 'analyze_spending_trend') {
518
+ const apiKey = process.env.VERBAL_API_KEY || '';
519
+ const apiUrl = process.env.VERBAL_API_URL || 'http://localhost:3000';
520
+ if (!apiKey) {
521
+ throw new Error('VERBAL_API_KEY environment variable not set');
522
+ }
523
+ const timeframe = args.timeframe || '30d';
524
+ const metric = args.metric || 'cost';
525
+ const result = await analyzeSpendingTrend(apiKey, apiUrl, {
526
+ timeframe: timeframe,
527
+ metric: metric
528
+ });
529
+ return {
530
+ content: [
531
+ {
532
+ type: 'text',
533
+ text: result,
534
+ },
535
+ ],
536
+ };
537
+ }
538
+ if (name === 'get_model_efficiency') {
539
+ const apiKey = process.env.VERBAL_API_KEY || '';
540
+ const apiUrl = process.env.VERBAL_API_URL || 'http://localhost:3000';
541
+ if (!apiKey) {
542
+ throw new Error('VERBAL_API_KEY environment variable not set');
543
+ }
544
+ const timeframe = args.timeframe || '30d';
545
+ const result = await getModelEfficiency(apiKey, apiUrl, { timeframe: timeframe });
546
+ return {
547
+ content: [
548
+ {
549
+ type: 'text',
550
+ text: result,
551
+ },
552
+ ],
553
+ };
554
+ }
555
+ if (name === 'sync_tokscale') {
556
+ const syncConfig = {
557
+ apiKey: config.apiKey,
558
+ ingestUrl: config.endpoint,
559
+ hmacSecret: config.hmacSecret ?? undefined,
560
+ orgId: config.orgId ?? undefined,
561
+ batchSize: config.batchSize,
562
+ flushIntervalMs: 0,
563
+ httpTimeoutMs: config.httpTimeoutMs,
564
+ stateDir: path.join(os.homedir(), '.claude', 'verbal-logs'),
565
+ };
566
+ const syncOptions = {
567
+ since: typeof args.since === 'string' ? args.since : undefined,
568
+ until: typeof args.until === 'string' ? args.until : undefined,
569
+ platforms: Array.isArray(args.platforms)
570
+ ? args.platforms.filter((p) => typeof p === 'string')
571
+ : undefined,
572
+ project: typeof args.project === 'string' ? args.project : config.defaultProject,
573
+ };
574
+ const result = await syncFromTokscale(syncConfig, syncOptions);
575
+ return {
576
+ content: [
577
+ {
578
+ type: 'text',
579
+ text: `Tokscale sync complete: ${result.events} events, ${result.days} days, $${result.totalCost.toFixed(2)} total cost`,
580
+ },
581
+ ],
582
+ };
583
+ }
584
+ if (name === 'submit_session_review') {
585
+ const sessionId = ensureString(args.session_id);
586
+ if (!sessionId) {
587
+ throw new Error('submit_session_review requires a non-empty session_id');
588
+ }
589
+ const usefulnessRating = ensureNumber(args.usefulness_rating);
590
+ const effectivenessRating = ensureNumber(args.effectiveness_rating);
591
+ const timeSavedBucket = ensureString(args.time_saved_bucket);
592
+ if (usefulnessRating === undefined || effectivenessRating === undefined || !timeSavedBucket) {
593
+ throw new Error('submit_session_review requires usefulness_rating, effectiveness_rating, and time_saved_bucket');
594
+ }
595
+ const ratings = {
596
+ usefulness_rating: usefulnessRating,
597
+ effectiveness_rating: effectivenessRating,
598
+ time_saved_bucket: timeSavedBucket,
599
+ improvement_text: ensureString(args.improvement_text),
600
+ };
601
+ await sessionTracker.submitReview(sessionId, ratings);
602
+ return {
603
+ content: [
604
+ {
605
+ type: 'text',
606
+ text: `Session review submitted for session ${sessionId}`,
607
+ },
608
+ ],
609
+ };
610
+ }
611
+ if (name === 'get_pending_session_reviews') {
612
+ const pending = await sessionTracker.getPendingReviews();
613
+ if (pending.length === 0) {
614
+ return {
615
+ content: [
616
+ {
617
+ type: 'text',
618
+ text: 'No sessions pending review.',
619
+ },
620
+ ],
621
+ };
622
+ }
623
+ const lines = pending.map((s) => `- session_id: ${s.session_id} | start: ${s.session_start} | end: ${s.session_end} | events: ${s.event_count}`);
624
+ return {
625
+ content: [
626
+ {
627
+ type: 'text',
628
+ text: `${pending.length} session(s) pending review:\n${lines.join('\n')}`,
629
+ },
630
+ ],
631
+ };
632
+ }
633
+ if (name === 'optimize_prompt') {
634
+ const apiKey = process.env.VERBAL_API_KEY || '';
635
+ const apiUrl = process.env.VERBAL_API_URL || 'http://localhost:3000';
636
+ if (!apiKey) {
637
+ throw new Error('VERBAL_API_KEY environment variable not set');
638
+ }
639
+ const prompt = ensureString(args.prompt);
640
+ if (!prompt) {
641
+ throw new Error('optimize_prompt requires a non-empty prompt string');
642
+ }
643
+ const context = ensureString(args.context);
644
+ const thresholdRaw = ensureNumber(args.threshold);
645
+ const result = await optimizePromptTool({ prompt, context, threshold: thresholdRaw }, apiKey, apiUrl);
646
+ if (!result.available) {
647
+ return {
648
+ content: [
649
+ {
650
+ type: 'text',
651
+ text: `Prompt optimization is not available on your current plan. Upgrade at ${result.upgrade_url ?? '/pricing'}`,
652
+ },
653
+ ],
654
+ };
655
+ }
656
+ let text = `Prompt score: ${result.original_score}/100`;
657
+ if (result.optimized_prompt) {
658
+ text += `\nImprovement: +${result.improvement_delta} points\nOptimized prompt:\n\n${result.optimized_prompt}`;
659
+ }
660
+ else if (result.rewrite_triggered) {
661
+ text += '\nRewrite attempted but did not improve the prompt. Original is already good.';
662
+ }
663
+ else {
664
+ text += '\nPrompt is already strong (above threshold). No rewrite needed.';
665
+ }
666
+ if (result.issues && result.issues.length > 0) {
667
+ text += `\nIssues detected: ${result.issues.join(', ')}`;
668
+ }
669
+ return {
670
+ content: [{ type: 'text', text }],
671
+ };
672
+ }
673
+ throw new Error(`Unknown tool: ${name}`);
674
+ });
675
+ // Exported entry point — called by CLI `getverbal mcp-serve`
676
+ export async function startMcpServer() {
677
+ const transport = new StdioServerTransport();
678
+ await server.connect(transport);
679
+ console.error('[MCP] Verbal MCP server started with native SDK hooks support');
680
+ console.error(`[MCP] Ingesting to: ${config.endpoint || '(not configured)'}`);
681
+ }
682
+ // Auto-start when run directly (backwards compat during transition)
683
+ if (import.meta.url === `file://${process.argv[1]}`) {
684
+ startMcpServer().catch((err) => {
685
+ console.error('[MCP] Fatal error:', err);
686
+ process.exit(1);
687
+ });
688
+ }
689
+ //# sourceMappingURL=server.js.map