Skip to content

Commit 9da6852

Browse files
committed
feat: enhance query and request tools with better UX and source tracking
1 parent f5aa798 commit 9da6852

File tree

4 files changed

+298
-25
lines changed

4 files changed

+298
-25
lines changed

src/Mcp/Tools/QueriesTool.php

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,11 @@ public function handle(Request $request, EntriesRepository $repository): Respons
3131
return $this->getQueryDetails($id, $repository);
3232
}
3333

34+
// Check if filtering by path (most specific/direct UX)
35+
if ($path = $request->get('path')) {
36+
return $this->listQueriesForPath($path, $request, $repository);
37+
}
38+
3439
// Check if filtering by request_id
3540
if ($requestId = $request->get('request_id')) {
3641
return $this->listQueriesForRequest($requestId, $request, $repository);
@@ -45,8 +50,9 @@ public function handle(Request $request, EntriesRepository $repository): Respons
4550
public function schema(JsonSchema $schema): array
4651
{
4752
return [
48-
'id' => $schema->string()->description('ID of specific query to view details'),
49-
'request_id' => $schema->string()->description('Filter queries by the request ID they belong to (uses batch_id grouping)'),
53+
'id' => $schema->string()->description('ID of specific DATABASE QUERY to view details (Caution: do not use a request ID here)'),
54+
'path' => $schema->string()->description('Get queries for the most recent request matching this path (e.g., /api/users)'),
55+
'request_id' => $schema->string()->description('Filter queries by a specific Request ID (ID returned by the requests tool)'),
5056
'limit' => $schema->integer()->default(50)->description('Maximum number of queries to return'),
5157
'slow' => $schema->boolean()->default(false)->description('Filter only slow queries (>100ms)'),
5258
];
@@ -118,6 +124,17 @@ protected function listQueries(Request $request, EntriesRepository $repository):
118124
return Response::text($combinedText);
119125
}
120126

127+
protected function listQueriesForPath(string $path, Request $request, EntriesRepository $repository): Response
128+
{
129+
$requestId = $this->findRequestIdByPath($path);
130+
131+
if (!$requestId) {
132+
return Response::error("No request found for path: {$path}");
133+
}
134+
135+
return $this->listQueriesForRequest($requestId, $request, $repository);
136+
}
137+
121138
protected function listQueriesForRequest(string $requestId, Request $request, EntriesRepository $repository): Response
122139
{
123140
// Get the batch_id for this request
@@ -157,6 +174,8 @@ protected function listQueriesForRequest(string $requestId, Request $request, En
157174
'duration' => $duration,
158175
'connection' => $content['connection'] ?? 'default',
159176
'created_at' => $createdAt,
177+
'file' => $content['file'] ?? null,
178+
'line' => $content['line'] ?? null,
160179
];
161180
}
162181

@@ -198,9 +217,22 @@ protected function getQueryDetails(string $id, EntriesRepository $repository): R
198217
$entry = $repository->find($id);
199218

200219
if (!$entry) {
220+
// Smart Fallback: if not found, check if it's a request ID
221+
$batchId = $this->getBatchIdForRequest($id);
222+
if ($batchId) {
223+
return $this->listQueriesForRequest($id, new Request(['limit' => 50]), $repository);
224+
}
201225
return Response::error("Query not found: {$id}");
202226
}
203227

228+
if ($entry->type !== EntryType::QUERY) {
229+
// If ID belongs to another type (like request), and it has a batch, show its queries
230+
$batchId = $this->getBatchIdForRequest($id);
231+
if ($batchId) {
232+
return $this->listQueriesForRequest($id, new Request(['limit' => 50]), $repository);
233+
}
234+
}
235+
204236
$content = is_array($entry->content) ? $entry->content : [];
205237
$createdAt = isset($content['created_at']) ? DateFormatter::format($content['created_at']) : 'Unknown';
206238

@@ -211,6 +243,11 @@ protected function getQueryDetails(string $id, EntriesRepository $repository): R
211243
$output .= "Duration: " . number_format(($content['time'] ?? 0), 2) . "ms\n";
212244
$output .= "Created At: {$createdAt}\n\n";
213245

246+
// Location if available
247+
if (isset($content['file'])) {
248+
$output .= "Source: {$content['file']} at line {$content['line']}\n\n";
249+
}
250+
214251
// Full SQL
215252
$output .= "SQL:\n" . ($content['sql'] ?? 'Unknown') . "\n\n";
216253

@@ -219,13 +256,28 @@ protected function getQueryDetails(string $id, EntriesRepository $repository): R
219256
$output .= "Bindings:\n" . json_encode($content['bindings'], JSON_PRETTY_PRINT) . "\n";
220257
}
221258

259+
// Backtrace if available
260+
if (isset($content['backtrace']) && !empty($content['backtrace'])) {
261+
$output .= "Backtrace:\n";
262+
foreach (array_slice($content['backtrace'], 0, 5) as $frame) {
263+
$file = $frame['file'] ?? 'unknown';
264+
$line = $frame['line'] ?? '?';
265+
$output .= "- {$file}:{$line}\n";
266+
}
267+
}
268+
222269
$combinedText = $output . "\n\n--- JSON Data ---\n" . json_encode([
223270
'id' => $entry->id,
224271
'connection' => $content['connection'] ?? 'default',
225272
'duration' => $content['time'] ?? 0,
226273
'created_at' => $createdAt,
274+
'location' => [
275+
'file' => $content['file'] ?? null,
276+
'line' => $content['line'] ?? null,
277+
],
227278
'sql' => $content['sql'] ?? 'Unknown',
228279
'bindings' => $content['bindings'] ?? [],
280+
'backtrace' => $content['backtrace'] ?? [],
229281
], JSON_PRETTY_PRINT);
230282

231283
return Response::text($combinedText);

src/Mcp/Tools/RequestsTool.php

Lines changed: 76 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public function handle(Request $request, EntriesRepository $repository): Respons
2828
try {
2929
if ($id = $request->get('id')) {
3030
$includeRelated = $request->boolean('include_related', true);
31-
return $this->getRequestDetails($id, $includeRelated, $repository);
31+
$includeQueries = $request->boolean('include_queries', false);
32+
return $this->getRequestDetails($id, $includeRelated, $includeQueries, $repository);
3233
}
3334
return $this->listRequests($request, $repository);
3435
} catch (\Exception $e) {
@@ -44,7 +45,8 @@ public function schema(JsonSchema $schema): array
4445
'method' => $schema->string()->description('Filter by HTTP method'),
4546
'status' => $schema->integer()->description('Filter by status code'),
4647
'path' => $schema->string()->description('Filter by path'),
47-
'include_related' => $schema->boolean()->default(true),
48+
'include_related' => $schema->boolean()->default(true)->description('Include summary of related entries'),
49+
'include_queries' => $schema->boolean()->default(false)->description('Include detailed queries associated with this request'),
4850
];
4951
}
5052

@@ -61,7 +63,11 @@ protected function listRequests(Request $request, EntriesRepository $repository)
6163
$options->tag('status:' . $status);
6264
}
6365
if ($path = $request->get('path')) {
64-
$options->tag('path:' . $path);
66+
$uuids = $this->getRequestUuidsByPath($path, $limit);
67+
if (empty($uuids)) {
68+
return Response::text("No requests found for path: {$path}");
69+
}
70+
$options->uuids($uuids);
6571
}
6672

6773
$entries = $repository->get(EntryType::REQUEST, $options);
@@ -110,7 +116,7 @@ protected function listRequests(Request $request, EntriesRepository $repository)
110116
return Response::text($table);
111117
}
112118

113-
protected function getRequestDetails(string $id, bool $includeRelated, EntriesRepository $repository): Response
119+
protected function getRequestDetails(string $id, bool $includeRelated, bool $includeQueries, EntriesRepository $repository): Response
114120
{
115121
$entry = $repository->find($id);
116122
if (!$entry) {
@@ -128,27 +134,70 @@ protected function getRequestDetails(string $id, bool $includeRelated, EntriesRe
128134
$output .= "Created At: {$createdAt}\n";
129135

130136
$relatedSummary = [];
131-
if ($includeRelated && isset($entry->batchId) && $entry->batchId) {
132-
$summary = $this->getBatchSummary($entry->batchId);
133-
$typeLabels = ['query' => 'Queries', 'log' => 'Logs', 'cache' => 'Cache Operations',
134-
'model' => 'Model Events', 'view' => 'Views', 'exception' => 'Exceptions',
135-
'event' => 'Events', 'job' => 'Jobs', 'mail' => 'Mails',
136-
'notification' => 'Notifications', 'redis' => 'Redis Operations'];
137-
138-
$output .= "\n--- Related Entries ---\n";
139-
$hasRelated = false;
140-
foreach ($summary as $type => $count) {
141-
if ($type !== 'request') {
142-
$label = $typeLabels[$type] ?? ucfirst($type);
143-
$output .= "- {$label}: {$count}\n";
144-
$relatedSummary[$type] = $count;
145-
$hasRelated = true;
137+
$batchQueries = [];
138+
139+
if (($includeRelated || $includeQueries) && isset($entry->batchId) && $entry->batchId) {
140+
if ($includeRelated) {
141+
$summary = $this->getBatchSummary($entry->batchId);
142+
$typeLabels = [
143+
'query' => 'Queries',
144+
'log' => 'Logs',
145+
'cache' => 'Cache Operations',
146+
'model' => 'Model Events',
147+
'view' => 'Views',
148+
'exception' => 'Exceptions',
149+
'event' => 'Events',
150+
'job' => 'Jobs',
151+
'mail' => 'Mails',
152+
'notification' => 'Notifications',
153+
'redis' => 'Redis Operations'
154+
];
155+
156+
$output .= "\n--- Related Entries ---\n";
157+
$hasRelated = false;
158+
foreach ($summary as $type => $count) {
159+
if ($type !== 'request') {
160+
$label = $typeLabels[$type] ?? ucfirst($type);
161+
$output .= "- {$label}: {$count}\n";
162+
$relatedSummary[$type] = $count;
163+
$hasRelated = true;
164+
}
165+
}
166+
if ($hasRelated) {
167+
$output .= "\nTip: Use 'queries --request_id={$id}' to see queries for this request.\n";
168+
} else {
169+
$output .= "(No related entries found)\n";
146170
}
147171
}
148-
if ($hasRelated) {
149-
$output .= "\nTip: Use 'queries --request_id={$id}' to see queries for this request.\n";
150-
} else {
151-
$output .= "(No related entries found)\n";
172+
173+
if ($includeQueries) {
174+
$queryEntries = $this->getEntriesByBatchId($entry->batchId, 'query', 10);
175+
if (!empty($queryEntries)) {
176+
$output .= "\n--- Associated Queries (Top 10) ---\n";
177+
foreach ($queryEntries as $q) {
178+
$sql = $q->content['sql'] ?? 'Unknown';
179+
$time = $q->content['time'] ?? 0;
180+
$location = isset($q->content['file']) ? " ({$q->content['file']}:{$q->content['line']})" : "";
181+
182+
$output .= sprintf(
183+
"[%s] %-50s (%s ms)%s\n",
184+
$q->id,
185+
strlen($sql) > 50 ? substr($sql, 0, 47) . '...' : $sql,
186+
number_format($time, 2),
187+
$location
188+
);
189+
190+
$batchQueries[] = [
191+
'id' => $q->id,
192+
'sql' => $sql,
193+
'duration' => $time,
194+
'location' => [
195+
'file' => $q->content['file'] ?? null,
196+
'line' => $q->content['line'] ?? null,
197+
]
198+
];
199+
}
200+
}
152201
}
153202
}
154203

@@ -178,6 +227,10 @@ protected function getRequestDetails(string $id, bool $includeRelated, EntriesRe
178227
$jsonData['related_entries'] = $relatedSummary;
179228
}
180229

230+
if (!empty($batchQueries)) {
231+
$jsonData['queries'] = $batchQueries;
232+
}
233+
181234
$output .= "\n\n--- JSON Data ---\n" . json_encode($jsonData, JSON_PRETTY_PRINT);
182235
return Response::text($output);
183236
}

src/Mcp/Tools/Traits/BatchQuerySupport.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,68 @@ protected function getBatchIdForRequest(string $requestId): ?string
134134

135135
return $batchId;
136136
}
137+
138+
/**
139+
* Find the most recent request UUID for a given path.
140+
*
141+
* @param string $path The URL path
142+
* @return string|null The UUID or null if not found
143+
*/
144+
protected function findRequestIdByPath(string $path): ?string
145+
{
146+
$uuids = $this->getRequestUuidsByPath($path, 1);
147+
return $uuids[0] ?? null;
148+
}
149+
150+
/**
151+
* Search for request UUIDs by path.
152+
*
153+
* @param string $path The URL path
154+
* @param int $limit Max results
155+
* @return array Array of UUID strings
156+
*/
157+
protected function getRequestUuidsByPath(string $path, int $limit = 50): array
158+
{
159+
try {
160+
// Normalize path for search
161+
$path = ltrim($path, '/');
162+
$fullPath = '/' . $path;
163+
164+
// Try searching by tag first (fastest)
165+
$uuidsByTag = DB::connection($this->getTelescopeConnection())
166+
->table('telescope_entries_tags')
167+
->join('telescope_entries', 'telescope_entries.uuid', '=', 'telescope_entries_tags.entry_uuid')
168+
->where('telescope_entries.type', 'request')
169+
->where(function ($query) use ($path, $fullPath) {
170+
$query->where('tag', 'path:' . $fullPath)
171+
->orWhere('tag', 'path:' . $path)
172+
->orWhere('tag', 'path:/' . $path);
173+
})
174+
->orderBy('telescope_entries.sequence', 'desc')
175+
->limit($limit)
176+
->pluck('entry_uuid')
177+
->all();
178+
179+
if (count($uuidsByTag) > 0) {
180+
return $uuidsByTag;
181+
}
182+
183+
// Fallback: search by content column (more reliable but slower)
184+
return DB::connection($this->getTelescopeConnection())
185+
->table('telescope_entries')
186+
->where('type', 'request')
187+
->where(function ($query) use ($path) {
188+
// Search for path string in various JSON formats
189+
$jsonPath = str_replace('/', '\\/', $path);
190+
$query->where('content', 'like', '%' . $path . '%')
191+
->orWhere('content', 'like', '%' . $jsonPath . '%');
192+
})
193+
->orderBy('sequence', 'desc')
194+
->limit($limit)
195+
->pluck('uuid')
196+
->all();
197+
} catch (\Exception $e) {
198+
return [];
199+
}
200+
}
137201
}

0 commit comments

Comments
 (0)