@@ -6,12 +6,17 @@ import {
66 type CallToolResult ,
77 type Tool ,
88} from '@modelcontextprotocol/sdk/types.js' ;
9+ import { createRequire } from 'node:module' ;
910import { z } from 'zod' ;
1011import { api , ApiError } from '../lib/api.js' ;
1112import { readConfig } from '../lib/config.js' ;
1213import { resolveProject } from '../lib/resolve.js' ;
1314import { parseDuration } from '../lib/format.js' ;
1415
16+ const pkg = createRequire ( import . meta. url ) ( '../../package.json' ) as { version : string } ;
17+
18+ const DEFAULT_ENTRY_LIMIT = 50 ;
19+
1520// Each tool's input schema is described twice: once as a Zod schema for runtime
1621// validation (parse the args before calling the API), once as JSON Schema for
1722// the MCP `tools/list` response (clients show this to the model).
@@ -38,84 +43,150 @@ const listEntriesInput = z.object({
3843 project : z . string ( ) . optional ( ) ,
3944 startDate : z . string ( ) . optional ( ) . describe ( 'ISO-8601 earliest start time' ) ,
4045 endDate : z . string ( ) . optional ( ) . describe ( 'ISO-8601 latest start time' ) ,
41- limit : z . number ( ) . int ( ) . positive ( ) . optional ( ) ,
46+ limit : z . number ( ) . int ( ) . min ( 1 ) . max ( 500 ) . optional ( ) ,
4247} ) ;
4348
4449const TOOLS : Tool [ ] = [
4550 {
4651 name : 'whoami' ,
47- description : 'Return the currently authenticated Timebook user.' ,
52+ description : 'Return the currently authenticated Timebook user (id, email, name) .' ,
4853 inputSchema : { type : 'object' , properties : { } , additionalProperties : false } ,
54+ annotations : { readOnlyHint : true , idempotentHint : true , openWorldHint : true } ,
4955 } ,
5056 {
5157 name : 'list_projects' ,
52- description : 'List all projects available to the current token.' ,
58+ description :
59+ 'List all projects available to the current token. Returns id, name, and client for each project.' ,
5360 inputSchema : { type : 'object' , properties : { } , additionalProperties : false } ,
61+ annotations : { readOnlyHint : true , idempotentHint : true , openWorldHint : true } ,
5462 } ,
5563 {
5664 name : 'list_clients' ,
5765 description : 'List all clients available to the current token.' ,
5866 inputSchema : { type : 'object' , properties : { } , additionalProperties : false } ,
67+ annotations : { readOnlyHint : true , idempotentHint : true , openWorldHint : true } ,
5968 } ,
6069 {
6170 name : 'get_active_timer' ,
62- description : 'Return the currently running timer, or null if none.' ,
71+ description :
72+ 'Return the currently running timer (project, description, started_at), or null if no timer is running.' ,
6373 inputSchema : { type : 'object' , properties : { } , additionalProperties : false } ,
74+ annotations : { readOnlyHint : true , idempotentHint : true , openWorldHint : true } ,
6475 } ,
6576 {
6677 name : 'start_timer' ,
6778 description :
68- 'Start a timer on a project. Stops any other running timer first ( Timebook allows only one active timer) .' ,
79+ 'Start a timer on a project. Stops any other running timer first — Timebook allows only one active timer at a time .' ,
6980 inputSchema : {
7081 type : 'object' ,
7182 properties : {
72- project : { type : 'string' , description : 'Project id or exact name' } ,
73- description : { type : 'string' } ,
74- rate : { type : 'string' , description : 'Rate id or exact name' } ,
83+ project : {
84+ type : 'string' ,
85+ description : 'Project id (UUID) or exact project name. Use list_projects to discover.' ,
86+ } ,
87+ description : {
88+ type : 'string' ,
89+ description : 'What the user is working on (visible in the time entry).' ,
90+ } ,
91+ rate : {
92+ type : 'string' ,
93+ description : 'Optional rate id (UUID) or exact rate name (e.g. "Software Development").' ,
94+ } ,
7595 } ,
7696 required : [ 'project' ] ,
7797 additionalProperties : false ,
7898 } ,
99+ annotations : {
100+ readOnlyHint : false ,
101+ destructiveHint : false ,
102+ idempotentHint : false ,
103+ openWorldHint : true ,
104+ } ,
79105 } ,
80106 {
81107 name : 'stop_timer' ,
82- description : 'Stop the currently running timer.' ,
108+ description :
109+ 'Stop the currently running timer. Returns { stopped: false } if no timer was running.' ,
83110 inputSchema : { type : 'object' , properties : { } , additionalProperties : false } ,
111+ annotations : {
112+ readOnlyHint : false ,
113+ destructiveHint : true ,
114+ idempotentHint : true ,
115+ openWorldHint : true ,
116+ } ,
84117 } ,
85118 {
86119 name : 'log_time' ,
87120 description :
88- 'Log a manual time entry. Provide either `duration`, or both `startTime` and `endTime`.' ,
121+ 'Log a manual (past) time entry. Provide either `duration` (relative to now) , or both `startTime` and `endTime` (absolute ISO-8601 timestamps) .' ,
89122 inputSchema : {
90123 type : 'object' ,
91124 properties : {
92- project : { type : 'string' , description : 'Project id or exact name' } ,
93- description : { type : 'string' } ,
125+ project : {
126+ type : 'string' ,
127+ description : 'Project id (UUID) or exact project name.' ,
128+ } ,
129+ description : {
130+ type : 'string' ,
131+ description : 'What the user worked on.' ,
132+ } ,
94133 duration : {
95134 type : 'string' ,
96- description : 'e.g. "1h", "45m", "1h30m", "1.5h", "1:30", or "90" (minutes)' ,
135+ description :
136+ 'How long the work took. Accepts "1h", "45m", "1h30m", "1.5h", "1:30", or "90" (interpreted as minutes).' ,
137+ } ,
138+ startTime : {
139+ type : 'string' ,
140+ description :
141+ 'ISO-8601 start time (e.g. "2026-05-04T09:00:00Z"). Required if duration is omitted.' ,
142+ } ,
143+ endTime : {
144+ type : 'string' ,
145+ description : 'ISO-8601 end time. Required if duration is omitted.' ,
146+ } ,
147+ rate : {
148+ type : 'string' ,
149+ description : 'Optional rate id or exact rate name (e.g. "Software Development").' ,
97150 } ,
98- startTime : { type : 'string' , description : 'ISO-8601' } ,
99- endTime : { type : 'string' , description : 'ISO-8601' } ,
100- rate : { type : 'string' } ,
101151 } ,
102152 required : [ 'project' ] ,
103153 additionalProperties : false ,
104154 } ,
155+ annotations : {
156+ readOnlyHint : false ,
157+ destructiveHint : false ,
158+ idempotentHint : false ,
159+ openWorldHint : true ,
160+ } ,
105161 } ,
106162 {
107163 name : 'list_entries' ,
108- description : ' List recent time entries, optionally filtered by project and date range.' ,
164+ description : ` List recent time entries, optionally filtered by project and/or date range. Returns at most ${ DEFAULT_ENTRY_LIMIT } entries by default; pass a higher \`limit\` to see more.` ,
109165 inputSchema : {
110166 type : 'object' ,
111167 properties : {
112- project : { type : 'string' , description : 'Project id or exact name' } ,
113- startDate : { type : 'string' , description : 'ISO-8601' } ,
114- endDate : { type : 'string' , description : 'ISO-8601' } ,
115- limit : { type : 'number' } ,
168+ project : {
169+ type : 'string' ,
170+ description : 'Optional project id or exact name. Omit to list across all projects.' ,
171+ } ,
172+ startDate : {
173+ type : 'string' ,
174+ description : 'ISO-8601 — only entries whose start time is on or after this.' ,
175+ } ,
176+ endDate : {
177+ type : 'string' ,
178+ description : 'ISO-8601 — only entries whose start time is on or before this.' ,
179+ } ,
180+ limit : {
181+ type : 'integer' ,
182+ description : `Maximum number of entries to return. Defaults to ${ DEFAULT_ENTRY_LIMIT } .` ,
183+ minimum : 1 ,
184+ maximum : 500 ,
185+ } ,
116186 } ,
117187 additionalProperties : false ,
118188 } ,
189+ annotations : { readOnlyHint : true , idempotentHint : true , openWorldHint : true } ,
119190 } ,
120191] ;
121192
@@ -229,8 +300,8 @@ async function handleTool(name: string, args: unknown): Promise<ToolResult> {
229300 startDate : input . startDate ,
230301 endDate : input . endDate ,
231302 } ) ;
232- const limited = input . limit ? entries . slice ( 0 , input . limit ) : entries ;
233- return ok ( limited ) ;
303+ const limit = input . limit ?? DEFAULT_ENTRY_LIMIT ;
304+ return ok ( entries . slice ( 0 , limit ) ) ;
234305 }
235306 default :
236307 return err ( `Unknown tool: ${ name } ` ) ;
@@ -248,7 +319,7 @@ async function handleTool(name: string, args: unknown): Promise<ToolResult> {
248319
249320export async function runMcpServer ( ) : Promise < void > {
250321 const server = new Server (
251- { name : 'timebook' , version : '0.1.1' } ,
322+ { name : 'timebook' , version : pkg . version } ,
252323 { capabilities : { tools : { } } } ,
253324 ) ;
254325
0 commit comments