@@ -143,6 +143,7 @@ import { registerQueryExecutionTools } from "./tools/query-execution.js";
143143import { registerQueryPlanTools } from "./tools/query-plan.js" ;
144144import { registerQuerySessionTools } from "./tools/query-sessions.js" ;
145145import { registerQueryTemplateTools } from "./tools/query-templates.js" ;
146+ import { registerRecentRowsTools } from "./tools/recent-rows.js" ;
146147import { registerSearchCatalogTools } from "./tools/search-catalog.js" ;
147148import { registerSqlUsageTools } from "./tools/sql-usages.js" ;
148149import { registerTableContextTools } from "./tools/table-context.js" ;
@@ -219,6 +220,7 @@ export function createObMcpServer(
219220 registerQueryPlanTools ( registerTool , runtime , context ) ;
220221 registerQueryBuildTools ( registerTool , runtime , context ) ;
221222 registerQueryTemplateTools ( registerTool , runtime , context ) ;
223+ registerRecentRowsTools ( registerTool , runtime , context ) ;
222224 registerSearchCatalogTools ( registerTool , runtime , context ) ;
223225 registerSqlUsageTools ( registerTool , runtime , context ) ;
224226
@@ -491,86 +493,6 @@ export function createObMcpServer(
491493 }
492494 ) ;
493495
494- registerTool (
495- "ob_find_recent_rows" ,
496- {
497- title : "Find Recent Rows" ,
498- description : "Read recent rows from an allowed table using an explicit or metadata-detected time column with safe pagination." ,
499- inputSchema : {
500- ...profileInput ,
501- table : z . string ( ) . min ( 1 ) ,
502- schema : z . string ( ) . optional ( ) ,
503- timeColumn : z . string ( ) . min ( 1 ) . optional ( ) ,
504- columns : z . array ( z . string ( ) . min ( 1 ) ) . max ( 100 ) . optional ( ) ,
505- page : z . number ( ) . int ( ) . positive ( ) . optional ( ) . default ( 1 ) ,
506- pageSize : z . number ( ) . int ( ) . positive ( ) . max ( 1_000 ) . optional ( )
507- }
508- } ,
509- async ( { profile, table, schema, timeColumn, columns, page = 1 , pageSize } ) => {
510- const selected = selectAuthorizedProfile ( runtime , profile , context ) ;
511- const effectiveSchema = metadataSchema ( selected . config , schema ) ;
512- assertTableAccess ( effectiveSchema , table , policyContext ( selected . config ) ) ;
513- const metadataRows = await withQuerySlot ( selected , ( ) =>
514- describeTable ( selected . pool , selected . config . compatibility , table , effectiveSchema , selected . config . queryTimeoutMs )
515- ) ;
516- const visibleColumns = filterColumnMetadata ( metadataRows , selected . config . policy ) ;
517- const resolvedTime = resolveRecentRowsTimeColumn ( visibleColumns , selected . config , effectiveSchema , table , timeColumn ) ;
518- const requestedColumns = recentRowsProjection ( columns , resolvedTime . column ) ;
519- const built = buildStructuredSelect (
520- selected . config ,
521- effectiveSchema ,
522- table ,
523- visibleColumns ,
524- {
525- columns : requestedColumns ,
526- filters : [ ] ,
527- orderBy : [ { column : resolvedTime . column , direction : "desc" } ] ,
528- pageSize,
529- page
530- }
531- ) ;
532- const sqlParams = built . params as SqlParam [ ] ;
533- assertSqlInputLimits ( built . sql , sqlParams , selected . config ) ;
534- assertSqlPolicy ( built . sql , policyContext ( selected . config ) ) ;
535- const output = await withQuerySlot ( selected , ( ) =>
536- runReadOnlyQuery (
537- selected . pool ,
538- selected . config . compatibility ,
539- built . sql ,
540- sqlParams ,
541- built . pagination . pageSize ,
542- selected . config . queryTimeoutMs ,
543- selected . config . enforceReadOnlyTransaction ,
544- selected . config . maxResultBytes
545- )
546- ) ;
547- assertOutputAllowed ( output , selected . config . policy ) ;
548- const visibleOutput = enrichQueryOutput (
549- output ,
550- selected . config ,
551- recentRowsWarnings ( built . warnings , resolvedTime , output . truncated )
552- ) ;
553- const result = {
554- mode : selected . config . compatibility ,
555- schema : effectiveSchema ,
556- table,
557- timeColumn : resolvedTime ,
558- sql : built . sql ,
559- paramCount : sqlParams . length ,
560- page : built . pagination . page ,
561- pageSize : built . pagination . pageSize ,
562- columns : visibleOutput . columns ,
563- rows : visibleOutput . rows ,
564- markdownPreview : visibleOutput . markdownPreview ,
565- truncated : visibleOutput . truncated ,
566- warnings : visibleOutput . warnings ,
567- executed : true
568- } ;
569- assertJsonByteLimit ( result , selected . config . maxResultBytes ) ;
570- return toResult ( result ) ;
571- }
572- ) ;
573-
574496 registerTool (
575497 "ob_check_data_quality" ,
576498 {
@@ -2784,146 +2706,6 @@ function valueDistributionWarnings(
27842706 return [ ...warnings ] ;
27852707}
27862708
2787- function detectTimeColumnCandidates (
2788- rows : unknown [ ] ,
2789- config : ObConfig ,
2790- schema : string | undefined ,
2791- table : string ,
2792- maxColumns : number
2793- ) {
2794- return rows
2795- . map ( ( row , index ) => {
2796- const metadata = profileColumnMetadata ( row , config . compatibility ) ;
2797- if ( ! metadata ) {
2798- return undefined ;
2799- }
2800- const signal = timeColumnSignal ( metadata . name , metadata . dataType , metadata . comment ) ;
2801- if ( signal . confidence < 0.35 ) {
2802- return undefined ;
2803- }
2804- return {
2805- column : metadata . name ,
2806- kind : signal . kind ,
2807- confidence : signal . confidence ,
2808- evidence : signal . evidence ,
2809- dataType : metadata . dataType ,
2810- nullable : metadata . nullable ,
2811- comment : metadata . comment ,
2812- masked : shouldMaskColumnValue ( metadata . name , config , schema , table ) ,
2813- ordinalPosition : numberRecordValue ( row as Record < string , unknown > , "ordinal_position" , "ORDINAL_POSITION" , "column_id" , "COLUMN_ID" ) ?? index + 1
2814- } ;
2815- } )
2816- . filter ( ( item ) : item is NonNullable < typeof item > => Boolean ( item ) )
2817- . sort ( ( left , right ) => right . confidence - left . confidence || left . ordinalPosition - right . ordinalPosition )
2818- . slice ( 0 , Math . max ( 1 , Math . min ( maxColumns , 50 ) ) ) ;
2819- }
2820-
2821- function timeColumnSignal (
2822- columnName : string ,
2823- dataType : string | undefined ,
2824- comment : string | undefined
2825- ) : { kind : string ; confidence : number ; evidence : string [ ] } {
2826- const normalized = columnName . trim ( ) . toLowerCase ( ) ;
2827- const commentText = ( comment ?? "" ) . trim ( ) . toLowerCase ( ) ;
2828- const typeCategory = profileColumnTypeCategory ( dataType ) ;
2829- const evidence : string [ ] = [ ] ;
2830- let confidence = 0 ;
2831- let kind = "generic_time" ;
2832-
2833- if ( typeCategory === "temporal" ) {
2834- confidence += 0.35 ;
2835- evidence . push ( `Data type "${ dataType } " is temporal.` ) ;
2836- }
2837- if ( / ^ ( c r e a t e d _ a t | c r e a t e d _ t i m e | c r e a t e _ t i m e | g m t _ c r e a t e | i n s e r t _ t i m e | c r e a t e d ) $ / . test ( normalized ) ) {
2838- confidence += 0.5 ;
2839- kind = "created_time" ;
2840- evidence . push ( "Column name matches common create-time naming." ) ;
2841- } else if ( / ^ ( u p d a t e d _ a t | u p d a t e d _ t i m e | u p d a t e _ t i m e | m o d i f i e d _ a t | m o d i f i e d _ t i m e | g m t _ m o d i f i e d | l a s t _ u p d a t e | l a s t _ m o d i f i e d ) $ / . test ( normalized ) ) {
2842- confidence += 0.5 ;
2843- kind = "updated_time" ;
2844- evidence . push ( "Column name matches common update-time naming." ) ;
2845- } else if ( / ( e v e n t | o c c u r | t r a d e | o r d e r | p a y | p a i d | b i z | b u s i n e s s | e f f e c t i v e | s t a r t | e n d ) .* ( t i m e | d a t e | d a y ) $ / . test ( normalized ) || / ^ ( b i z _ d a t e | b u s i n e s s _ d a t e ) $ / . test ( normalized ) ) {
2846- confidence += 0.45 ;
2847- kind = "business_time" ;
2848- evidence . push ( "Column name suggests a business event time." ) ;
2849- } else if ( / ( ^ | _ ) ( t i m e | d a t e | d a y | t i m e s t a m p ) $ / . test ( normalized ) || / ( t i m e | d a t e | t i m e s t a m p ) / . test ( normalized ) ) {
2850- confidence += 0.2 ;
2851- evidence . push ( "Column name contains a time-related token." ) ;
2852- }
2853- if ( / ( c r e a t e d | c r e a t e t i m e | u p d a t e d | u p d a t e t i m e | m o d i f i e d | e v e n t t i m e | b u s i n e s s t i m e | o c c u r r e d | t r a d e t i m e | p a y t i m e ) / . test ( commentText ) ) {
2854- confidence += 0.15 ;
2855- evidence . push ( "Column comment contains time-related wording." ) ;
2856- }
2857- return {
2858- kind,
2859- confidence : Math . min ( 0.98 , Number ( confidence . toFixed ( 2 ) ) ) ,
2860- evidence
2861- } ;
2862- }
2863-
2864- function resolveRecentRowsTimeColumn (
2865- rows : unknown [ ] ,
2866- config : ObConfig ,
2867- schema : string | undefined ,
2868- table : string ,
2869- requestedTimeColumn : string | undefined
2870- ) {
2871- if ( requestedTimeColumn ) {
2872- const column = getBuildColumn ( config , buildSelectColumnInfos ( config , schema , table , rows ) , requestedTimeColumn ) ;
2873- const signal = timeColumnSignal ( column . name , column . type , column . comment ) ;
2874- return {
2875- column : column . name ,
2876- source : "explicit" ,
2877- confidence : Math . max ( signal . confidence , 0.5 ) ,
2878- kind : signal . kind ,
2879- evidence : signal . evidence . length > 0 ? signal . evidence : [ "Time column was supplied explicitly." ] ,
2880- dataType : column . type ,
2881- masked : column . masked
2882- } ;
2883- }
2884- const candidates = detectTimeColumnCandidates ( rows , config , schema , table , 5 ) ;
2885- const best = candidates [ 0 ] ;
2886- if ( ! best || best . confidence < 0.5 ) {
2887- throw new Error ( "No reliable time column was detected. Pass timeColumn explicitly." ) ;
2888- }
2889- return {
2890- column : best . column ,
2891- source : "detected" ,
2892- confidence : best . confidence ,
2893- kind : best . kind ,
2894- evidence : best . evidence ,
2895- dataType : best . dataType ,
2896- masked : best . masked ,
2897- alternatives : candidates . slice ( 1 , 5 ) . map ( ( candidate ) => ( {
2898- column : candidate . column ,
2899- kind : candidate . kind ,
2900- confidence : candidate . confidence ,
2901- evidence : candidate . evidence
2902- } ) )
2903- } ;
2904- }
2905-
2906- function recentRowsProjection ( columns : string [ ] | undefined , timeColumn : string ) : string [ ] | undefined {
2907- if ( ! columns || columns . length === 0 ) {
2908- return undefined ;
2909- }
2910- return uniqueStrings ( [ timeColumn , ...columns ] ) ;
2911- }
2912-
2913- function recentRowsWarnings ( baseWarnings : string [ ] , timeColumn : { source : string ; alternatives ?: unknown [ ] } , truncated : boolean ) : string [ ] {
2914- const warnings = new Set ( baseWarnings ) ;
2915- if ( timeColumn . source === "detected" ) {
2916- warnings . add ( "Time column was selected from metadata detection; pass timeColumn explicitly if this is not the business time field." ) ;
2917- }
2918- if ( Array . isArray ( timeColumn . alternatives ) && timeColumn . alternatives . length > 0 ) {
2919- warnings . add ( "Other time-like columns were found; review alternatives before using this query for business decisions." ) ;
2920- }
2921- if ( truncated ) {
2922- warnings . add ( "Rows were truncated by the configured row or output limit." ) ;
2923- }
2924- return [ ...warnings ] ;
2925- }
2926-
29272709function selectDataQualityColumns (
29282710 config : ObConfig ,
29292711 columns : Array < { name : string ; type ?: string ; nullable ?: boolean ; roles : string [ ] ; masked : boolean } > ,
0 commit comments