@timofi/context-server 1.0.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.
package/README.md ADDED
@@ -0,0 +1,572 @@
1
+ # Timofi Context Server
2
+
3
+ <img alt="gitleaks badge" src="https://img.shields.io/badge/protected%20by-gitleaks-blue">
4
+
5
+ A secure, scalable repository-aware memory and code intelligence platform for AI assistants. The system consists of two main components: a **Timofi API Server** for centralized processing and secure token storage, and a lightweight **MCP Client** for local AI assistant integration.
6
+
7
+ ## Architecture
8
+
9
+ ```mermaid
10
+ flowchart TD
11
+ %% -------- CLIENT LAYER --------
12
+ subgraph CLIENTS["🖥️ Client Applications"]
13
+ CC[Claude Code]
14
+ CHAT[ChatGPT]
15
+ IDE[IDEs & Editors]
16
+ CLI[CLI Tools]
17
+ end
18
+
19
+ %% -------- MCP CLIENT (LOCAL) --------
20
+ subgraph MCP_CLIENT["📡 MCP Client (Local)"]
21
+ MCP_SERVER[MCP Protocol Handler]
22
+ API_CLIENT[Timofi API Client]
23
+ CONFIG[Config Manager]
24
+ end
25
+
26
+ %% -------- TIMOFI API SERVER (CENTRALIZED) --------
27
+ subgraph API_SERVER["🏗️ Timofi API Server (Centralized)"]
28
+ AUTH_API[Authentication Service]
29
+ TOKEN_VAULT[GitHub Token Vault]
30
+ MEMORY_API[Memory Service API]
31
+ CODE_API[Code Intelligence API]
32
+ REPO_API[Repository Service API]
33
+ WORKER[Background Workers]
34
+ end
35
+
36
+ %% -------- DATA LAYER --------
37
+ subgraph DATA_LAYER["💾 Data Storage (Server-Side)"]
38
+ POSTGRES[(PostgreSQL)]
39
+ QDRANT[(Vector Database)]
40
+ NEO4J[(Graph Database)]
41
+ REDIS[(Cache Layer)]
42
+ end
43
+
44
+ %% -------- EXTERNAL SERVICES --------
45
+ subgraph EXTERNAL["🌐 External Services"]
46
+ GITHUB[GitHub API]
47
+ EMBEDDING[Embedding Services]
48
+ end
49
+
50
+ %% Connections
51
+ CLIENTS --> MCP_CLIENT
52
+ MCP_CLIENT --> API_SERVER
53
+ API_SERVER --> DATA_LAYER
54
+ API_SERVER --> EXTERNAL
55
+
56
+ %% Details
57
+ MCP_SERVER -.->|"Single API Key"| API_CLIENT
58
+ AUTH_API -.->|"Secure Storage"| TOKEN_VAULT
59
+ TOKEN_VAULT -.->|"On Behalf Of Users"| GITHUB
60
+ ```
61
+
62
+ ## Features
63
+
64
+ ### 🔒 Secure Architecture
65
+
66
+ - **Centralized Token Management**: GitHub tokens stored securely on the API server
67
+ - **Single API Key**: Users only need one Timofi API key locally
68
+ - **Zero Local Secrets**: No sensitive credentials stored on user devices
69
+ - **Enterprise Security**: End-to-end encryption, audit logs, and access controls
70
+
71
+ ### 🧠 Repository-Scoped Memory
72
+
73
+ - **Intelligent Memory**: Context-aware memory storage and retrieval per repository
74
+ - **Vector Embeddings**: Semantic search across conversational history and insights
75
+ - **Multi-Repository Support**: Seamless handling of organization-wide repositories
76
+ - **Privacy by Design**: Repository-scoped data isolation with secure access controls
77
+
78
+ ### 🔍 Advanced Code Intelligence
79
+
80
+ - **Real-time Code Analysis**: Live symbol extraction and relationship mapping
81
+ - **Semantic Code Search**: Natural language queries for code discovery
82
+ - **Background Indexing**: Automated repository processing with progress tracking
83
+ - **Cross-Reference Analysis**: Find related functions, tests, and documentation
84
+
85
+ ### 🔗 Universal Integration
86
+
87
+ - **MCP Protocol**: Compatible with Claude Code, ChatGPT, and major IDEs
88
+ - **API First**: RESTful API for custom integrations and advanced use cases
89
+ - **Lightweight Client**: Minimal local footprint with powerful remote capabilities
90
+ - **Developer Friendly**: Simple setup with comprehensive documentation
91
+
92
+ ## Installation
93
+
94
+ ### Quick Start (Recommended)
95
+
96
+ 1. **Install MCP Client**
97
+ ```bash
98
+ npm install -g @goldcode-io/timofi-context-server
99
+ ```
100
+
101
+ 2. **Get Your API Key**
102
+ - Visit [Timofi Console](https://console.timofi.goldcode.io)
103
+ - Sign up with GitHub OAuth
104
+ - Generate your Timofi API key
105
+ - Authorize GitHub repositories you want to access
106
+
107
+ 3. **Configure Claude Desktop**
108
+ ```json
109
+ {
110
+ "mcpServers": {
111
+ "timofi": {
112
+ "command": "timofi-context-server",
113
+ "env": {
114
+ "TIMOFI_API_KEY": "your-api-key-here"
115
+ }
116
+ }
117
+ }
118
+ }
119
+ ```
120
+
121
+ That's it! No local secrets, no complex setup, no Docker containers to manage.
122
+
123
+ ### Self-Hosted Installation (Advanced)
124
+
125
+ For organizations requiring complete control over data and infrastructure:
126
+
127
+ #### Prerequisites
128
+ - Node.js 18+
129
+ - Docker and Docker Compose
130
+ - PostgreSQL, Qdrant, Neo4j, Redis access
131
+
132
+ #### API Server Setup
133
+
134
+ 1. **Clone and Install API Server**
135
+ ```bash
136
+ git clone https://github.com/goldcode-io/timofi-api-server.git
137
+ cd timofi-api-server
138
+ npm install
139
+ ```
140
+
141
+ 2. **Configure Environment**
142
+ ```bash
143
+ cp .env.example .env
144
+ # Configure database connections, encryption keys, etc.
145
+ ```
146
+
147
+ 3. **Start Infrastructure**
148
+ ```bash
149
+ docker-compose up -d # PostgreSQL, Qdrant, Neo4j, Redis
150
+ npm run migrate # Run database migrations
151
+ npm start # Start API server
152
+ ```
153
+
154
+ #### MCP Client Setup
155
+
156
+ 1. **Install Local MCP Client**
157
+ ```bash
158
+ npm install -g @goldcode-io/timofi-context-server
159
+ ```
160
+
161
+ 2. **Configure for Self-Hosted**
162
+ ```json
163
+ {
164
+ "mcpServers": {
165
+ "timofi": {
166
+ "command": "timofi-context-server",
167
+ "env": {
168
+ "TIMOFI_API_URL": "http://localhost:3000",
169
+ "TIMOFI_API_KEY": "your-self-hosted-key"
170
+ }
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ ## Configuration
177
+
178
+ ### MCP Client Configuration
179
+
180
+ The MCP client requires minimal configuration:
181
+
182
+ ```json
183
+ {
184
+ "mcpServers": {
185
+ "timofi": {
186
+ "command": "timofi-context-server",
187
+ "env": {
188
+ "TIMOFI_API_KEY": "your-api-key-here",
189
+ "TIMOFI_API_URL": "https://api.timofi.goldcode.io" // Optional: defaults to hosted service
190
+ }
191
+ }
192
+ }
193
+ }
194
+ ```
195
+
196
+ ### API Server Configuration (Self-Hosted Only)
197
+
198
+ ```bash
199
+ # Database Configuration
200
+ DATABASE_URL=postgresql://user:pass@localhost:5432/timofi
201
+ QDRANT_URL=http://localhost:6333
202
+ NEO4J_URL=bolt://localhost:7687
203
+ REDIS_URL=redis://localhost:6379
204
+
205
+ # Security Configuration
206
+ ENCRYPTION_KEY=your-32-byte-encryption-key
207
+ JWT_SECRET=your-jwt-secret
208
+ GITHUB_WEBHOOK_SECRET=your-webhook-secret
209
+
210
+ # External Services
211
+ OPENAI_API_KEY=your-openai-key # For embeddings
212
+ GITHUB_APP_ID=your-github-app-id
213
+ GITHUB_APP_PRIVATE_KEY=your-github-app-private-key
214
+
215
+ # Server Configuration
216
+ PORT=3000
217
+ LOG_LEVEL=info
218
+ NODE_ENV=production
219
+
220
+ # Webhooks: public URL of this API (required for auto-created GitHub webhooks)
221
+ # GitHub will send push events here; use your real API host (e.g. https://api.yourdomain.com)
222
+ API_BASE_URL=https://your-api-host.com
223
+ ```
224
+
225
+ ### Repository Access Setup
226
+
227
+ 1. **Via Web Console (Recommended)**
228
+ - Visit [Timofi Console](https://console.timofi.goldcode.io)
229
+ - Connect your GitHub account
230
+ - Select repositories to authorize
231
+ - Generate scoped access tokens
232
+
233
+ 2. **Via API (Advanced)**
234
+ ```bash
235
+ curl -X POST https://api.timofi.goldcode.io/v1/auth/github/connect \
236
+ -H "Authorization: Bearer YOUR_TIMOFI_API_KEY" \
237
+ -H "Content-Type: application/json" \
238
+ -d '{"github_token": "ghp_your_github_token"}'
239
+ ```
240
+
241
+ ### Webhooks (push → re-index)
242
+
243
+ When a webhook is active for a repository, **pushes to the default branch** create a pending indexing task; the worker picks it up within ~30 seconds and runs a new index.
244
+
245
+ **1. Auto-creation on connect**
246
+ When you connect a repository (Dashboard or `POST /api/v1/repositories/connect`), the server tries to create a GitHub webhook. For that to work:
247
+
248
+ - Set **`API_BASE_URL`** (or `NEXT_PUBLIC_API_URL`) to the **public URL of your API server** (e.g. `https://api.yourdomain.com`). GitHub must be able to reach this URL; do not use `localhost` in production.
249
+ - The user connecting the repo must have a GitHub token stored (Dashboard → Settings → GitHub tokens).
250
+
251
+ **2. Create webhook manually** (if auto-creation failed or the repo was connected earlier without it)
252
+
253
+ - **Dashboard:** Repositories → select the repo → **Manage webhook** (webhook icon) → **Create Webhook**. The default URL is the current page origin + `/api/v1/webhooks/github`. If your API runs on a different host than the dashboard, enter the full API URL (e.g. `https://api.yourdomain.com/api/v1/webhooks/github`).
254
+ - **API:**
255
+ `POST /api/v1/repositories/:id/webhooks`
256
+ Body: `{ "webhookUrl": "https://your-api-host/api/v1/webhooks/github", "events": ["push", "repository", "delete"], "active": true }`
257
+ Requires authentication and a GitHub token for a user with access to the repo.
258
+
259
+ **3. Check**
260
+ - In GitHub: repo → Settings → Webhooks. You should see a webhook pointing to your API URL; “Recent Deliveries” shows push events.
261
+ - Only pushes to the **default branch** (e.g. `main`) trigger indexing.
262
+
263
+ **4. 401 Invalid signature**
264
+ - The webhook **secret** is not visible anywhere (stored in the app DB and in GitHub). If you get 401, the secret may not match (e.g. webhook was edited in GitHub with a new secret).
265
+ - **Fix:** Dashboard → Repositories → select repo → Manage webhook → **Regenerate secret** (updates both GitHub and the app).
266
+ - Or via API: `POST /api/v1/repositories/:id/webhooks/regenerate` with body `{ "webhookUrl": "https://your-api-host/api/v1/webhooks/github" }` (requires auth and a GitHub token).
267
+
268
+ ## Usage
269
+
270
+ ### Command Line Interface
271
+
272
+ ```bash
273
+ # Start MCP server for a specific repository
274
+ timofi-context --repository owner/repo-name
275
+
276
+ # Start with custom configuration
277
+ timofi-context --repository owner/repo-name --config /path/to/config.json
278
+
279
+ # Index a repository
280
+ timofi-context index --repository owner/repo-name
281
+
282
+ # Query memories
283
+ timofi-context query --repository owner/repo-name --query "authentication logic"
284
+
285
+ # List supported repositories
286
+ timofi-context list-repos
287
+ ```
288
+
289
+ ### MCP Tools
290
+
291
+ The server provides the following MCP tools:
292
+
293
+ #### Memory Management
294
+
295
+ - `store_memory`: Store conversation context or code insights
296
+ - `retrieve_memories`: Get relevant memories for current context
297
+ - `search_memories`: Semantic search across stored memories
298
+
299
+ #### Code Retrieval
300
+
301
+ - `search_code`: Find relevant code snippets and symbols
302
+ - `get_file_context`: Retrieve file content with related symbols
303
+ - `find_tests`: Locate tests related to specific code
304
+ - `get_dependencies`: Analyze code dependencies and relationships
305
+
306
+ #### Repository Management
307
+
308
+ - `index_repository`: Index a repository for search and memory
309
+ - `check_access`: Verify user access to repository
310
+ - `get_repo_info`: Retrieve repository metadata and structure
311
+
312
+ ## Self-Hosted Services
313
+
314
+ ### Docker Compose Services
315
+
316
+ The included `docker-compose.yml` provides:
317
+
318
+ - **Mem0**: Memory management service
319
+ - **Sourcegraph**: Code intelligence and search
320
+ - **Qdrant**: Vector database for embeddings
321
+ - **Redis**: Caching layer for improved performance
322
+
323
+ ### Service Configuration
324
+
325
+ #### Mem0 Configuration
326
+
327
+ ```yaml
328
+ mem0:
329
+ image: mem0ai/mem0:latest
330
+ ports:
331
+ - '8000:8000'
332
+ environment:
333
+ - VECTOR_STORE_URL=http://qdrant:6333
334
+ volumes:
335
+ - mem0_data:/app/data
336
+ ```
337
+
338
+ #### Sourcegraph Configuration
339
+
340
+ ```yaml
341
+ sourcegraph:
342
+ image: sourcegraph/server:latest
343
+ ports:
344
+ - '3080:7080'
345
+ environment:
346
+ - SRC_LOG_LEVEL=warn
347
+ volumes:
348
+ - sourcegraph_data:/var/opt/sourcegraph
349
+ ```
350
+
351
+ ## API Reference
352
+
353
+ ### Authentication
354
+
355
+ All requests require a valid GitHub token that provides access to the target repository:
356
+
357
+ ```bash
358
+ curl -H "Authorization: Bearer ghp_your_token" \
359
+ -H "X-Repository: owner/repo-name" \
360
+ http://localhost:3000/api/memories
361
+ ```
362
+
363
+ ### Memory Endpoints
364
+
365
+ #### Store Memory
366
+
367
+ ```bash
368
+ POST /api/memories
369
+ {
370
+ "content": "User discussed authentication implementation",
371
+ "context": {
372
+ "file": "src/auth.js",
373
+ "function": "validateToken",
374
+ "conversation_id": "conv_123"
375
+ },
376
+ "repository": "owner/repo-name"
377
+ }
378
+ ```
379
+
380
+ #### Retrieve Memories
381
+
382
+ ```bash
383
+ GET /api/memories?query=authentication&repository=owner/repo-name&limit=10
384
+ ```
385
+
386
+ ### Code Search Endpoints
387
+
388
+ #### Search Code
389
+
390
+ ```bash
391
+ GET /api/code/search?q=authentication&repository=owner/repo-name&type=function
392
+ ```
393
+
394
+ #### Get File Context
395
+
396
+ ```bash
397
+ GET /api/code/context?file=src/auth.js&repository=owner/repo-name
398
+ ```
399
+
400
+ ## Development
401
+
402
+ ### Project Structure
403
+
404
+ ```
405
+ timofi-context-server/
406
+ ├── src/
407
+ │ ├── server.ts # MCP server implementation
408
+ │ ├── memory/ # Memory management
409
+ │ │ ├── mem0.ts
410
+ │ │ └── zep.ts
411
+ │ ├── code/ # Code retrieval
412
+ │ │ ├── sourcegraph.ts
413
+ │ │ └── llamaindex.ts
414
+ │ ├── auth/ # GitHub authentication
415
+ │ └── utils/ # Utilities
416
+ ├── docker-compose.yml # Self-hosted services
417
+ ├── package.json
418
+ └── README.md
419
+ ```
420
+
421
+ ### Scripts
422
+
423
+ ```bash
424
+ # Development
425
+ npm run dev # Start with hot reload
426
+ npm run build # Build for production
427
+ npm run test # Run tests
428
+ npm run lint # Lint code
429
+
430
+ # Docker
431
+ npm run docker:up # Start all services
432
+ npm run docker:down # Stop all services
433
+ npm run docker:logs # View service logs
434
+
435
+ # Utilities
436
+ npm run index-repo # Index a repository
437
+ npm run clean-cache # Clear all caches
438
+ ```
439
+
440
+ ### Testing
441
+
442
+ ```bash
443
+ # Unit tests
444
+ npm test
445
+
446
+ # Integration tests with services
447
+ npm run test:integration
448
+
449
+ # Test specific repository
450
+ npm run test -- --repository owner/repo-name
451
+ ```
452
+
453
+ ## Security
454
+
455
+ ### Access Control
456
+
457
+ - Repository access verified via GitHub API before each request
458
+ - GitHub tokens validated and scoped appropriately
459
+ - Memory and code data isolated by repository
460
+
461
+ ### Data Privacy
462
+
463
+ - All data stored in self-hosted services
464
+ - No data sent to external services without explicit configuration
465
+ - Configurable data retention policies
466
+
467
+ ### Best Practices
468
+
469
+ - Use fine-grained GitHub tokens with minimal required permissions
470
+ - Regularly rotate API keys and tokens
471
+ - Monitor access logs for suspicious activity
472
+ - Keep self-hosted services updated
473
+
474
+ ## Troubleshooting
475
+
476
+ ### Common Issues
477
+
478
+ #### Service Connection Errors
479
+
480
+ ```bash
481
+ # Check service status
482
+ docker-compose ps
483
+
484
+ # View service logs
485
+ docker-compose logs mem0
486
+ docker-compose logs sourcegraph
487
+ ```
488
+
489
+ #### GitHub Authentication
490
+
491
+ ```bash
492
+ # Test token validity
493
+ curl -H "Authorization: Bearer ghp_your_token" \
494
+ https://api.github.com/user
495
+
496
+ # Check repository access
497
+ curl -H "Authorization: Bearer ghp_your_token" \
498
+ https://api.github.com/repos/owner/repo-name
499
+ ```
500
+
501
+ #### MCP Connection Issues
502
+
503
+ ```bash
504
+ # Test MCP server
505
+ timofi-context --test-connection
506
+
507
+ # Debug mode
508
+ timofi-context --repository owner/repo-name --debug
509
+ ```
510
+
511
+ ### Logs
512
+
513
+ Service logs are available at:
514
+
515
+ - MCP Server: `~/.timofi-context/logs/server.log`
516
+ - Memory Service: `docker-compose logs mem0`
517
+ - Code Service: `docker-compose logs sourcegraph`
518
+
519
+ ## Contributing
520
+
521
+ 1. Fork the repository
522
+ 2. Create a feature branch: `git checkout -b feature/amazing-feature`
523
+ 3. Commit changes: `git commit -m 'Add amazing feature'`
524
+ 4. Push to branch: `git push origin feature/amazing-feature`
525
+ 5. Open a Pull Request
526
+
527
+ ## License
528
+
529
+ MIT License - see [LICENSE](LICENSE) file for details.
530
+
531
+ ## Support
532
+
533
+ - 📧 Email: support@goldcode.io
534
+ - 🐛 Issues: [GitHub Issues](https://github.com/goldcode-io/timofi-context-server/issues)
535
+ - 📖 Documentation: [Wiki](https://github.com/goldcode-io/timofi-context-server/wiki)
536
+
537
+ ## Secret Scanning with Gitleaks
538
+
539
+ This repository uses Gitleaks to detect hardcoded secrets and prevent sensitive data from being committed.
540
+ To guarantee consistent behavior between local development and CI, all scans are performed as filesystem scans (not Git history scans).
541
+
542
+ There are two scanning modes, depending on when and how they are executed.
543
+
544
+ ### 1. Github Actions Scans (Pull Requests and Manual)
545
+
546
+ Gitleaks is executed on:
547
+ - Every Pull Request
548
+ - Manually: via GitHub Actions → Run workflow
549
+
550
+ **What is scanned automatically ?**
551
+ - On PRs: The diff between the current state of the repository filesystem compared to your changes
552
+ - Manually: All filesystem on (trunk/master/main/default)
553
+ - Git history and other branches are not scanned
554
+
555
+ This is the primary protection mechanism for day-to-day development.
556
+
557
+ ### 2. Running a full scan locally
558
+
559
+ Developers can run a full scan locally at any time:
560
+ ```shell
561
+ $ make gitleaks
562
+ ```
563
+ This executes a full filesystem scan using the project’s `.gitleaks.local.toml` configuration.
564
+ Make sure you have Gitleaks installed locally ([GitLeaks Repository](https://github.com/gitleaks/gitleaks?tab=readme-ov-file#installing)).
565
+
566
+ ### About .gitleaks.toml file
567
+ The `.gitleaks.local.toml` and `.gitleaks.ci.toml` files defines allowlists and rules to reduce noise, such as:
568
+ - test files and fixtures
569
+ - examples and documentation
570
+ - .env files and templates
571
+
572
+ This keeps results actionable and avoids reporting known non-issues.
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{Server as T}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as b}from"@modelcontextprotocol/sdk/server/stdio.js";import{InitializeRequestSchema as P,ListToolsRequestSchema as I,ListResourcesRequestSchema as E,PingRequestSchema as O,CallToolRequestSchema as k}from"@modelcontextprotocol/sdk/types.js";import{EventEmitter as L}from"events";import{randomUUID as M}from"crypto";import{createLogger as C,format as a,transports as y}from"winston";var c=class u{logger;requestContext;constructor(e="info",t="development"){let r=t==="development";this.logger=C({level:e,format:a.combine(a.timestamp(),a.errors({stack:!0}),a.json(),r?a.prettyPrint():a.uncolorize()),defaultMeta:{service:"timofi-context-server",version:process.env.npm_package_version||"1.0.0"},transports:[new y.Console({stderrLevels:["error","warn","info","debug"],format:r?a.combine(a.colorize(),a.timestamp({format:"HH:mm:ss"}),a.printf(({timestamp:i,level:s,message:o,service:n,requestId:l,...g})=>{let f=Object.keys(g).length?` ${JSON.stringify(g)}`:"",w=l?` [${l}]`:"";return`${i} ${s}${w}: ${o}${f}`})):a.json()})]}),t==="production"&&(this.logger.add(new y.File({filename:"logs/error.log",level:"error",maxsize:10*1024*1024,maxFiles:5})),this.logger.add(new y.File({filename:"logs/combined.log",maxsize:10*1024*1024,maxFiles:10})))}setRequestContext(e){this.requestContext=e}clearRequestContext(){this.requestContext=void 0}formatMessage(e,t){let r={...t,requestId:t?.requestId||this.requestContext?.requestId,timestamp:new Date().toISOString()};return this.requestContext?.startTime&&!r.duration&&(r.duration=Date.now()-this.requestContext.startTime),[e,r]}debug(e,t){let[r,i]=this.formatMessage(e,t);this.logger.debug(r,i)}info(e,t){let[r,i]=this.formatMessage(e,t);this.logger.info(r,i)}warn(e,t){let[r,i]=this.formatMessage(e,t);this.logger.warn(r,i)}error(e,t,r){let[i,s]=this.formatMessage(e,r),o={...s,...t&&{error:{name:t.name,message:t.message,stack:t.stack,...t.constructor.name!=="Error"&&{type:t.constructor.name}}}};this.logger.error(i,o)}mcpRequest(e,t,r){this.info("MCP request received",{...r,method:e,params:t?JSON.stringify(t):void 0})}mcpResponse(e,t,r,i){let s=`MCP request ${t?"completed":"failed"}`,o={...i,method:e,success:t,duration:r};t?this.info(s,o):this.warn(s,o)}serverEvent(e,t,r){this.info(`Server event: ${e}`,{...r,event:e,details:t?JSON.stringify(t):void 0})}performanceMetric(e,t,r,i){this.info(`Performance: ${e}`,{...i,operation:e,duration:t,...r})}child(e){let t=new u;return t.logger=this.logger.child(e),t.requestContext=this.requestContext,t}},v;function q(){return v||(v=new c),v}var z=q();import x from"axios";var p=class{logger;cache;accessOrder;maxSize;defaultTTL;constructor(e=1e3,t=3e5){this.logger=new c("ResponseCache"),this.cache=new Map,this.accessOrder=[],this.maxSize=e,this.defaultTTL=t}generateKey(e,t,r){let i=`${e}:${t}`;if(r){let s=JSON.stringify(r,Object.keys(r).sort());return`${i}:${s}`}return i}get(e,t,r){let i=this.generateKey(e,t,r),s=this.cache.get(i);return s?Date.now()-s.timestamp>s.ttl?(this.logger.debug("Cache expired",{key:i}),this.cache.delete(i),this.accessOrder=this.accessOrder.filter(n=>n!==i),null):(this.updateAccessOrder(i),this.logger.debug("Cache hit",{key:i}),s.data):(this.logger.debug("Cache miss",{key:i}),null)}set(e,t,r,i,s){let o=this.generateKey(e,t,i);this.cache.size>=this.maxSize&&!this.cache.has(o)&&this.evictLRU();let n={data:r,timestamp:Date.now(),ttl:s||this.defaultTTL};this.cache.set(o,n),this.updateAccessOrder(o),this.logger.debug("Cache set",{key:o,ttl:n.ttl})}invalidate(e){let t=0,r=new RegExp(e);for(let i of this.cache.keys())r.test(i)&&(this.cache.delete(i),this.accessOrder=this.accessOrder.filter(s=>s!==i),t++);return this.logger.info("Cache invalidated",{pattern:e,count:t}),t}clear(){let e=this.cache.size;this.cache.clear(),this.accessOrder=[],this.logger.info("Cache cleared",{size:e})}getStats(){return{size:this.cache.size,maxSize:this.maxSize,hitRate:0}}updateAccessOrder(e){this.accessOrder=this.accessOrder.filter(t=>t!==e),this.accessOrder.push(e)}evictLRU(){if(this.accessOrder.length===0)return;let e=this.accessOrder.shift();this.cache.delete(e),this.logger.debug("LRU eviction",{key:e})}cleanup(){let e=Date.now(),t=0;for(let[r,i]of this.cache.entries())e-i.timestamp>i.ttl&&(this.cache.delete(r),this.accessOrder=this.accessOrder.filter(s=>s!==r),t++);return t>0&&this.logger.debug("Cache cleanup",{removed:t}),t}};import{v4 as S}from"uuid";var d=class{logger;queue;maxSize;maxRetries;isProcessing;processCallback;constructor(e=100,t=3){this.logger=new c("OfflineQueue"),this.queue=[],this.maxSize=e,this.maxRetries=t,this.isProcessing=!1}setProcessCallback(e){this.processCallback=e}enqueue(e,t,r){if(this.queue.length>=this.maxSize){let s=this.queue.shift();this.logger.warn("Queue full, removed oldest request",{removed:s})}let i={id:S(),method:e,endpoint:t,data:r,timestamp:Date.now(),retries:0};return this.queue.push(i),this.logger.info("Request queued",{id:i.id,method:e,endpoint:t,queueSize:this.queue.length}),i.id}async processQueue(){if(this.isProcessing){this.logger.debug("Queue already processing");return}if(this.queue.length===0){this.logger.debug("Queue empty");return}if(!this.processCallback){this.logger.error("No process callback set");return}this.isProcessing=!0,this.logger.info("Processing queue",{queueSize:this.queue.length});let e=[],t=[];for(let r of this.queue)try{await this.processCallback(r),e.push(r.id),this.logger.info("Request processed",{id:r.id})}catch(i){this.logger.error("Failed to process request",i instanceof Error?i:new Error(String(i)),{id:r.id,retries:r.retries}),r.retries++,r.retries<this.maxRetries?t.push(r):this.logger.error("Request exceeded max retries, discarding",new Error("Max retries exceeded"),{id:r.id,maxRetries:this.maxRetries})}this.queue=t,this.logger.info("Queue processing complete",{processed:e.length,failed:t.length}),this.isProcessing=!1}getStatus(){return{size:this.queue.length,maxSize:this.maxSize,isProcessing:this.isProcessing,oldestTimestamp:this.queue[0]?.timestamp}}clear(){let e=this.queue.length;this.queue=[],this.logger.info("Queue cleared",{size:e})}getQueue(){return[...this.queue]}remove(e){let t=this.queue.length;this.queue=this.queue.filter(i=>i.id!==e);let r=this.queue.length<t;return r&&this.logger.info("Request removed from queue",{requestId:e}),r}};var m=class{logger;config;http;cache;offlineQueue;isOnline;constructor(e){this.logger=new c("TimofiAPIClient"),this.config={apiUrl:e.apiUrl,apiKey:e.apiKey,timeout:e.timeout||3e4,retries:e.retries||3,retryDelay:e.retryDelay||1e3,cacheEnabled:e.cacheEnabled!==!1,cacheTTL:e.cacheTTL||3e5,offlineQueueEnabled:e.offlineQueueEnabled!==!1,http2:e.http2||!1},this.http=x.create({baseURL:this.config.apiUrl,timeout:this.config.timeout,headers:{Authorization:`Bearer ${this.config.apiKey}`,"Content-Type":"application/json"}}),this.cache=new p(1e3,this.config.cacheTTL),this.offlineQueue=new d(100,this.config.retries),this.offlineQueue.setProcessCallback(async t=>{await this.requestWithRetry(t.method,t.endpoint,t.data,1,!1)}),this.isOnline=!0,setInterval(()=>this.cache.cleanup(),6e4),this.logger.info("API client initialized",{apiUrl:this.config.apiUrl,cacheEnabled:this.config.cacheEnabled,offlineQueueEnabled:this.config.offlineQueueEnabled})}async storeMemory(e){return this.post("/api/v1/memories",e)}async searchMemories(e){let t=`search:${e.repository}:${e.query}`;return this.get("/api/v1/memories",e,t)}async getMemory(e){return this.get(`/api/v1/memories/${e}`)}async deleteMemory(e){await this.delete(`/api/v1/memories/${e}`)}async searchCode(e){let t=`code:${e.repository}:${e.query}`,r={repository:e.repository,q:e.query,language:e.language,path:e.path,limit:e.limit};return this.get("/api/v1/code/search",r,t)}async getFileContext(e){let t=`file:${e.repository}:${e.filePath}`;return this.get(`/api/v1/code/files/${encodeURIComponent(e.filePath)}`,{repository:e.repository,lineStart:e.lineStart,lineEnd:e.lineEnd},t)}async getProjectOverview(e){let t=`overview:${e.repository}`;return this.get("/api/v1/code/overview",{repository:e.repository},t)}async getSymbolInfo(e,t){return this.get(`/api/v1/code/symbols/${e}`,{repository:t})}async getSymbolReferences(e,t){return this.get(`/api/v1/code/symbols/${e}/references`,{repository:t})}async startIndexing(e){return this.post(`/api/v1/repositories/${e.owner}/${e.repo}/index`,{incremental:e.incremental,options:{branch:e.branch}})}async getRepository(e,t){return this.get(`/api/v1/repositories/${e}/${t}`)}async getIndexingTask(e){return this.get(`/api/v1/workers/tasks/${e.taskId}`)}async cancelIndexingTask(e){await this.delete(`/api/v1/workers/tasks/${e}`)}async listRepositories(){return this.get("/api/v1/repositories")}async get(e,t,r){if(this.config.cacheEnabled&&r){let s=this.cache.get("GET",e,t);if(s)return s}let i=await this.requestWithRetry("GET",e,t);return this.config.cacheEnabled&&r&&this.cache.set("GET",e,i,t),i}async post(e,t){return this.requestWithRetry("POST",e,t)}async delete(e){await this.requestWithRetry("DELETE",e)}async requestWithRetry(e,t,r,i=1,s=!0){try{let o={method:e,url:t};e==="GET"&&r?o.params=r:r&&(o.data=r);let n=await this.http.request(o);if(this.isOnline||(this.isOnline=!0,this.logger.info("API connection restored"),this.config.offlineQueueEnabled&&await this.offlineQueue.processQueue()),!n.data.success)throw new h(n.data.error||{message:"Unknown error"});return n.data.data}catch(o){if(x.isAxiosError(o)){let n=o;if(!n.response){if(this.logger.error("Network error",o,{endpoint:t,attempt:i}),this.isOnline&&(this.isOnline=!1,this.logger.warn("API connection lost")),s&&this.config.offlineQueueEnabled&&i===1)throw this.offlineQueue.enqueue(e,t,r),new Error("Request queued due to network error");if(i<this.config.retries){let l=Math.min(this.config.retryDelay*Math.pow(2,i-1),1e4);return this.logger.info("Retrying request",{endpoint:t,attempt:i,delay:l}),await this.sleep(l),this.requestWithRetry(e,t,r,i+1,!1)}throw new Error(`Network error after ${i} attempts`)}if(n.response.status===429){let l=n.response.data.error?.retryAfter||6e4;if(this.logger.warn("Rate limit exceeded",{endpoint:t,retryAfter:l}),i<this.config.retries)return await this.sleep(l),this.requestWithRetry(e,t,r,i+1,!1);throw new Error("Rate limit exceeded")}if(n.response.data?.error)throw new h(n.response.data.error)}throw this.logger.error("Request failed",o instanceof Error?o:void 0,{endpoint:t,attempt:i}),o}}sleep(e){return new Promise(t=>setTimeout(t,e))}invalidateCache(e){return this.cache.invalidate(e)}clearCache(){this.cache.clear()}getCacheStats(){return this.cache.getStats()}getQueueStatus(){return this.offlineQueue.getStatus()}async processOfflineQueue(){await this.offlineQueue.processQueue()}clearQueue(){this.offlineQueue.clear()}async healthCheck(){try{return await this.http.get("/health"),!0}catch{return!1}}},h=class extends Error{type;details;requestId;constructor(e){super(e.message||"API error"),this.name="APIClientError",this.type=e.type||"unknown_error",this.details=e.details,this.requestId=e.requestId}};var R=class extends L{server;transport;state="starting";logger;config;apiClient;startTime;shutdownTimer;activeRequests=new Map;processHandlers=[];constructor(e){super(),this.config={...e,environment:e.environment||"development",shutdownTimeout:e.shutdownTimeout||5e3},this.logger=new c("TimofiMCPClient"),this.startTime=new Date;let t={apiUrl:this.config.apiUrl,apiKey:this.config.apiKey,cacheEnabled:this.config.cacheEnabled,cacheTTL:this.config.cacheTTL,offlineQueueEnabled:this.config.offlineQueueEnabled};this.apiClient=new m(t),this.server=new T({name:e.name,version:e.version}),this.setupServerHandlers(),this.setupProcessHandlers()}setupServerHandlers(){this.server.setRequestHandler(P,async e=>{let t=this.generateRequestId(),r=this.createRequestContext(t,"initialize",e.params);this.logger.info("Initialize request",{params:e.params});try{let i={protocolVersion:"2024-11-05",capabilities:{tools:{},resources:{},prompts:{}},serverInfo:{name:this.config.name,version:this.config.version}};return this.logger.info("Initialize response",{duration:Date.now()-r.startTime}),i}catch(i){throw this.logger.error("Initialize request failed",i),i}finally{this.activeRequests.delete(t)}}),this.server.setRequestHandler(I,async()=>{let e=this.generateRequestId(),t=this.createRequestContext(e,"tools/list");this.logger.info("Tools list request");try{let r=this.getToolsList();return this.logger.info("Tools list response",{count:r.length,duration:Date.now()-t.startTime}),{tools:r}}catch(r){throw this.logger.error("Tools list request failed",r),r}finally{this.activeRequests.delete(e)}}),this.server.setRequestHandler(E,async()=>{let e=this.generateRequestId(),t=this.createRequestContext(e,"resources/list");this.logger.info("Resources list request");try{let r=[];return this.logger.info("Resources list response",{duration:Date.now()-t.startTime}),{resources:r}}catch(r){throw this.logger.error("Resources list request failed",r),r}finally{this.activeRequests.delete(e)}}),this.server.setRequestHandler(O,async()=>({status:"pong",timestamp:new Date().toISOString(),server:this.config.name,version:this.config.version})),this.server.setRequestHandler(k,async e=>{let t=this.generateRequestId(),r=this.createRequestContext(t,"tools/call",e.params);this.logger.info("Tool call request",{tool:e.params.name,args:e.params.arguments});try{let i=await this.handleToolCall(e.params.name,e.params.arguments||{});return this.logger.info("Tool call response",{tool:e.params.name,duration:Date.now()-r.startTime}),i}catch(i){throw this.logger.error("Tool call request failed",i,{tool:e.params.name}),i instanceof h?new Error(`API Error: ${i.message}`):i}finally{this.activeRequests.delete(t)}}),this.server.onerror=e=>{this.logger.error("MCP Server error",e),this.emit("error",{error:e}),this.state!=="shutting_down"&&this.setState("error")},this.logger.debug("MCP protocol handlers initialized")}setupProcessHandlers(){let e=()=>this.shutdown("SIGTERM"),t=()=>this.shutdown("SIGINT"),r=s=>{this.logger.error("Uncaught exception",s),this.shutdown("uncaught_exception")},i=s=>{this.logger.error("Unhandled rejection",s),this.shutdown("unhandled_rejection")};process.on("SIGTERM",e),process.on("SIGINT",t),process.on("uncaughtException",r),process.on("unhandledRejection",i),this.processHandlers=[{event:"SIGTERM",handler:e},{event:"SIGINT",handler:t},{event:"uncaughtException",handler:r},{event:"unhandledRejection",handler:i}]}cleanupProcessHandlers(){for(let{event:e,handler:t}of this.processHandlers)process.removeListener(e,t);this.processHandlers=[]}getToolsList(){return[{name:"store_memory",description:"Store a new memory in the context store for a repository",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"},content:{type:"string",description:"Content to remember"},context:{type:"object",description:"Additional context information"},metadata:{type:"object",description:"Additional metadata"}},required:["repository","content"]}},{name:"search_memories",description:"Search for memories related to a query",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"},query:{type:"string",description:"Search query"},limit:{type:"number",description:"Maximum number of results"},threshold:{type:"number",description:"Similarity threshold (0-1)"}},required:["repository","query"]}},{name:"search_code",description:"Search for code patterns across repository files",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"},query:{type:"string",description:"Search query"},language:{type:"string",description:"Filter by programming language"},path:{type:"string",description:"Filter by file path pattern"},limit:{type:"number",description:"Maximum number of results"}},required:["repository","query"]}},{name:"get_file_context",description:"Get the context of a specific file with symbols and structure",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"},filePath:{type:"string",description:"Path to the file"},lineStart:{type:"number",description:"Starting line number"},lineEnd:{type:"number",description:"Ending line number"}},required:["repository","filePath"]}},{name:"get_project_overview",description:"Get project overview: file tree structure, README, and key docs (CONTRIBUTING, LICENSE, etc.). Use this to understand the repository layout and documentation before searching code.",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"}},required:["repository"]}},{name:"list_repositories",description:"List all available repositories that have been connected and indexed",inputSchema:{type:"object",properties:{}}},{name:"start_indexing",description:"Start indexing a repository",inputSchema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},incremental:{type:"boolean",description:"Perform incremental indexing"},branch:{type:"string",description:"Branch to index"}},required:["owner","repo"]}},{name:"get_indexing_status",description:"Get the status of a repository indexing task",inputSchema:{type:"object",properties:{taskId:{type:"string",description:"Indexing task ID"}},required:["taskId"]}}]}async handleToolCall(e,t){switch(e){case"store_memory":return{content:[{type:"text",text:`Memory stored successfully with ID: ${(await this.apiClient.storeMemory({repository:t.repository,content:t.content,context:t.context,metadata:t.metadata})).memoryId}`}]};case"search_memories":let i=await this.apiClient.searchMemories({repository:t.repository,query:t.query,limit:t.limit,threshold:t.threshold});return{content:[{type:"text",text:JSON.stringify(i,null,2)}]};case"search_code":let s=await this.apiClient.searchCode({repository:t.repository,query:t.query,language:t.language,path:t.path,limit:t.limit});return{content:[{type:"text",text:JSON.stringify(s,null,2)}]};case"get_file_context":let o=await this.apiClient.getFileContext({repository:t.repository,filePath:t.filePath,lineStart:t.lineStart,lineEnd:t.lineEnd});return{content:[{type:"text",text:JSON.stringify(o,null,2)}]};case"get_project_overview":let n=await this.apiClient.getProjectOverview({repository:t.repository});return{content:[{type:"text",text:JSON.stringify(n,null,2)}]};case"list_repositories":let l=await this.apiClient.listRepositories();return{content:[{type:"text",text:JSON.stringify(l,null,2)}]};case"start_indexing":let g=await this.apiClient.startIndexing({owner:t.owner,repo:t.repo,incremental:t.incremental,branch:t.branch});return{content:[{type:"text",text:`Indexing started. Task ID: ${g.taskId}, Status: ${g.status}`}]};case"get_indexing_status":let f=await this.apiClient.getIndexingTask({taskId:t.taskId});return{content:[{type:"text",text:JSON.stringify(f,null,2)}]};default:throw new Error(`Unknown tool: ${e}`)}}generateRequestId(){return M()}createRequestContext(e,t,r){let i={requestId:e,timestamp:new Date,method:t,params:r,startTime:Date.now()};return this.activeRequests.set(e,i),i}setState(e){let t=this.state;this.state=e,this.logger.info("State change",{from:t,to:e})}async start(){try{if(this.setState("starting"),this.logger.info("Starting Timofi MCP Client",{name:this.config.name,version:this.config.version,apiUrl:this.config.apiUrl}),!await this.apiClient.healthCheck())throw new Error("API server is not accessible");this.transport=new b,await this.server.connect(this.transport),this.setState("ready"),this.logger.info("Timofi MCP Client started successfully"),this.emit("started",{config:this.config}),this.emit("ready",{uptime:this.getUptime()})}catch(e){throw this.setState("error"),this.logger.error("Failed to start MCP Client",e),this.emit("error",{error:e}),e}}async shutdown(e="manual"){if(this.state==="shutting_down"||this.state==="stopped")return;this.setState("shutting_down"),this.logger.info(`Shutting down client (reason: ${e})`);let t=new Promise(r=>{this.shutdownTimer=setTimeout(()=>{this.logger.warn("Forced shutdown after timeout"),r()},this.config.shutdownTimeout)});try{await Promise.race([this.waitForActiveRequests(),t]),await this.apiClient.processOfflineQueue(),this.server&&this.transport&&await this.server.close(),this.cleanupProcessHandlers(),this.setState("stopped"),this.logger.info("Client shutdown completed"),this.emit("shutdown",{reason:e})}catch(r){this.logger.error("Error during shutdown",r),this.setState("error"),this.emit("error",{error:r})}finally{this.shutdownTimer&&clearTimeout(this.shutdownTimer)}}async waitForActiveRequests(){let r=0;for(;this.activeRequests.size>0&&r<3e3;)await new Promise(i=>setTimeout(i,100)),r+=100;this.activeRequests.size>0&&this.logger.warn(`${this.activeRequests.size} requests still active during shutdown`)}getState(){return this.state}getUptime(){return Date.now()-this.startTime.getTime()}getConfig(){return{...this.config}}getActiveRequestsCount(){return this.activeRequests.size}getCacheStats(){return this.apiClient.getCacheStats()}getQueueStatus(){return this.apiClient.getQueueStatus()}invalidateCache(e){return this.apiClient.invalidateCache(e)}};export{R as TimofiMCPClient};
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import{Server as I}from"@modelcontextprotocol/sdk/server/index.js";import{StdioServerTransport as E}from"@modelcontextprotocol/sdk/server/stdio.js";import{InitializeRequestSchema as O,ListToolsRequestSchema as M,ListResourcesRequestSchema as L,PingRequestSchema as k,CallToolRequestSchema as A}from"@modelcontextprotocol/sdk/types.js";import{EventEmitter as _}from"events";import{randomUUID as Q}from"crypto";import{createLogger as S,format as l,transports as C}from"winston";var c=class n{logger;requestContext;constructor(e="info",t="development"){let r=t==="development";this.logger=S({level:e,format:l.combine(l.timestamp(),l.errors({stack:!0}),l.json(),r?l.prettyPrint():l.uncolorize()),defaultMeta:{service:"timofi-context-server",version:process.env.npm_package_version||"1.0.0"},transports:[new C.Console({stderrLevels:["error","warn","info","debug"],format:r?l.combine(l.colorize(),l.timestamp({format:"HH:mm:ss"}),l.printf(({timestamp:i,level:s,message:o,service:a,requestId:u,...p})=>{let v=Object.keys(p).length?` ${JSON.stringify(p)}`:"",q=u?` [${u}]`:"";return`${i} ${s}${q}: ${o}${v}`})):l.json()})]}),t==="production"&&(this.logger.add(new C.File({filename:"logs/error.log",level:"error",maxsize:10*1024*1024,maxFiles:5})),this.logger.add(new C.File({filename:"logs/combined.log",maxsize:10*1024*1024,maxFiles:10})))}setRequestContext(e){this.requestContext=e}clearRequestContext(){this.requestContext=void 0}formatMessage(e,t){let r={...t,requestId:t?.requestId||this.requestContext?.requestId,timestamp:new Date().toISOString()};return this.requestContext?.startTime&&!r.duration&&(r.duration=Date.now()-this.requestContext.startTime),[e,r]}debug(e,t){let[r,i]=this.formatMessage(e,t);this.logger.debug(r,i)}info(e,t){let[r,i]=this.formatMessage(e,t);this.logger.info(r,i)}warn(e,t){let[r,i]=this.formatMessage(e,t);this.logger.warn(r,i)}error(e,t,r){let[i,s]=this.formatMessage(e,r),o={...s,...t&&{error:{name:t.name,message:t.message,stack:t.stack,...t.constructor.name!=="Error"&&{type:t.constructor.name}}}};this.logger.error(i,o)}mcpRequest(e,t,r){this.info("MCP request received",{...r,method:e,params:t?JSON.stringify(t):void 0})}mcpResponse(e,t,r,i){let s=`MCP request ${t?"completed":"failed"}`,o={...i,method:e,success:t,duration:r};t?this.info(s,o):this.warn(s,o)}serverEvent(e,t,r){this.info(`Server event: ${e}`,{...r,event:e,details:t?JSON.stringify(t):void 0})}performanceMetric(e,t,r,i){this.info(`Performance: ${e}`,{...i,operation:e,duration:t,...r})}child(e){let t=new n;return t.logger=this.logger.child(e),t.requestContext=this.requestContext,t}},x;function P(){return x||(x=new c),x}var G=P();import R from"axios";var d=class{logger;cache;accessOrder;maxSize;defaultTTL;constructor(e=1e3,t=3e5){this.logger=new c("ResponseCache"),this.cache=new Map,this.accessOrder=[],this.maxSize=e,this.defaultTTL=t}generateKey(e,t,r){let i=`${e}:${t}`;if(r){let s=JSON.stringify(r,Object.keys(r).sort());return`${i}:${s}`}return i}get(e,t,r){let i=this.generateKey(e,t,r),s=this.cache.get(i);return s?Date.now()-s.timestamp>s.ttl?(this.logger.debug("Cache expired",{key:i}),this.cache.delete(i),this.accessOrder=this.accessOrder.filter(a=>a!==i),null):(this.updateAccessOrder(i),this.logger.debug("Cache hit",{key:i}),s.data):(this.logger.debug("Cache miss",{key:i}),null)}set(e,t,r,i,s){let o=this.generateKey(e,t,i);this.cache.size>=this.maxSize&&!this.cache.has(o)&&this.evictLRU();let a={data:r,timestamp:Date.now(),ttl:s||this.defaultTTL};this.cache.set(o,a),this.updateAccessOrder(o),this.logger.debug("Cache set",{key:o,ttl:a.ttl})}invalidate(e){let t=0,r=new RegExp(e);for(let i of this.cache.keys())r.test(i)&&(this.cache.delete(i),this.accessOrder=this.accessOrder.filter(s=>s!==i),t++);return this.logger.info("Cache invalidated",{pattern:e,count:t}),t}clear(){let e=this.cache.size;this.cache.clear(),this.accessOrder=[],this.logger.info("Cache cleared",{size:e})}getStats(){return{size:this.cache.size,maxSize:this.maxSize,hitRate:0}}updateAccessOrder(e){this.accessOrder=this.accessOrder.filter(t=>t!==e),this.accessOrder.push(e)}evictLRU(){if(this.accessOrder.length===0)return;let e=this.accessOrder.shift();this.cache.delete(e),this.logger.debug("LRU eviction",{key:e})}cleanup(){let e=Date.now(),t=0;for(let[r,i]of this.cache.entries())e-i.timestamp>i.ttl&&(this.cache.delete(r),this.accessOrder=this.accessOrder.filter(s=>s!==r),t++);return t>0&&this.logger.debug("Cache cleanup",{removed:t}),t}};import{v4 as b}from"uuid";var m=class{logger;queue;maxSize;maxRetries;isProcessing;processCallback;constructor(e=100,t=3){this.logger=new c("OfflineQueue"),this.queue=[],this.maxSize=e,this.maxRetries=t,this.isProcessing=!1}setProcessCallback(e){this.processCallback=e}enqueue(e,t,r){if(this.queue.length>=this.maxSize){let s=this.queue.shift();this.logger.warn("Queue full, removed oldest request",{removed:s})}let i={id:b(),method:e,endpoint:t,data:r,timestamp:Date.now(),retries:0};return this.queue.push(i),this.logger.info("Request queued",{id:i.id,method:e,endpoint:t,queueSize:this.queue.length}),i.id}async processQueue(){if(this.isProcessing){this.logger.debug("Queue already processing");return}if(this.queue.length===0){this.logger.debug("Queue empty");return}if(!this.processCallback){this.logger.error("No process callback set");return}this.isProcessing=!0,this.logger.info("Processing queue",{queueSize:this.queue.length});let e=[],t=[];for(let r of this.queue)try{await this.processCallback(r),e.push(r.id),this.logger.info("Request processed",{id:r.id})}catch(i){this.logger.error("Failed to process request",i instanceof Error?i:new Error(String(i)),{id:r.id,retries:r.retries}),r.retries++,r.retries<this.maxRetries?t.push(r):this.logger.error("Request exceeded max retries, discarding",new Error("Max retries exceeded"),{id:r.id,maxRetries:this.maxRetries})}this.queue=t,this.logger.info("Queue processing complete",{processed:e.length,failed:t.length}),this.isProcessing=!1}getStatus(){return{size:this.queue.length,maxSize:this.maxSize,isProcessing:this.isProcessing,oldestTimestamp:this.queue[0]?.timestamp}}clear(){let e=this.queue.length;this.queue=[],this.logger.info("Queue cleared",{size:e})}getQueue(){return[...this.queue]}remove(e){let t=this.queue.length;this.queue=this.queue.filter(i=>i.id!==e);let r=this.queue.length<t;return r&&this.logger.info("Request removed from queue",{requestId:e}),r}};var f=class{logger;config;http;cache;offlineQueue;isOnline;constructor(e){this.logger=new c("TimofiAPIClient"),this.config={apiUrl:e.apiUrl,apiKey:e.apiKey,timeout:e.timeout||3e4,retries:e.retries||3,retryDelay:e.retryDelay||1e3,cacheEnabled:e.cacheEnabled!==!1,cacheTTL:e.cacheTTL||3e5,offlineQueueEnabled:e.offlineQueueEnabled!==!1,http2:e.http2||!1},this.http=R.create({baseURL:this.config.apiUrl,timeout:this.config.timeout,headers:{Authorization:`Bearer ${this.config.apiKey}`,"Content-Type":"application/json"}}),this.cache=new d(1e3,this.config.cacheTTL),this.offlineQueue=new m(100,this.config.retries),this.offlineQueue.setProcessCallback(async t=>{await this.requestWithRetry(t.method,t.endpoint,t.data,1,!1)}),this.isOnline=!0,setInterval(()=>this.cache.cleanup(),6e4),this.logger.info("API client initialized",{apiUrl:this.config.apiUrl,cacheEnabled:this.config.cacheEnabled,offlineQueueEnabled:this.config.offlineQueueEnabled})}async storeMemory(e){return this.post("/api/v1/memories",e)}async searchMemories(e){let t=`search:${e.repository}:${e.query}`;return this.get("/api/v1/memories",e,t)}async getMemory(e){return this.get(`/api/v1/memories/${e}`)}async deleteMemory(e){await this.delete(`/api/v1/memories/${e}`)}async searchCode(e){let t=`code:${e.repository}:${e.query}`,r={repository:e.repository,q:e.query,language:e.language,path:e.path,limit:e.limit};return this.get("/api/v1/code/search",r,t)}async getFileContext(e){let t=`file:${e.repository}:${e.filePath}`;return this.get(`/api/v1/code/files/${encodeURIComponent(e.filePath)}`,{repository:e.repository,lineStart:e.lineStart,lineEnd:e.lineEnd},t)}async getProjectOverview(e){let t=`overview:${e.repository}`;return this.get("/api/v1/code/overview",{repository:e.repository},t)}async getSymbolInfo(e,t){return this.get(`/api/v1/code/symbols/${e}`,{repository:t})}async getSymbolReferences(e,t){return this.get(`/api/v1/code/symbols/${e}/references`,{repository:t})}async startIndexing(e){return this.post(`/api/v1/repositories/${e.owner}/${e.repo}/index`,{incremental:e.incremental,options:{branch:e.branch}})}async getRepository(e,t){return this.get(`/api/v1/repositories/${e}/${t}`)}async getIndexingTask(e){return this.get(`/api/v1/workers/tasks/${e.taskId}`)}async cancelIndexingTask(e){await this.delete(`/api/v1/workers/tasks/${e}`)}async listRepositories(){return this.get("/api/v1/repositories")}async get(e,t,r){if(this.config.cacheEnabled&&r){let s=this.cache.get("GET",e,t);if(s)return s}let i=await this.requestWithRetry("GET",e,t);return this.config.cacheEnabled&&r&&this.cache.set("GET",e,i,t),i}async post(e,t){return this.requestWithRetry("POST",e,t)}async delete(e){await this.requestWithRetry("DELETE",e)}async requestWithRetry(e,t,r,i=1,s=!0){try{let o={method:e,url:t};e==="GET"&&r?o.params=r:r&&(o.data=r);let a=await this.http.request(o);if(this.isOnline||(this.isOnline=!0,this.logger.info("API connection restored"),this.config.offlineQueueEnabled&&await this.offlineQueue.processQueue()),!a.data.success)throw new g(a.data.error||{message:"Unknown error"});return a.data.data}catch(o){if(R.isAxiosError(o)){let a=o;if(!a.response){if(this.logger.error("Network error",o,{endpoint:t,attempt:i}),this.isOnline&&(this.isOnline=!1,this.logger.warn("API connection lost")),s&&this.config.offlineQueueEnabled&&i===1)throw this.offlineQueue.enqueue(e,t,r),new Error("Request queued due to network error");if(i<this.config.retries){let u=Math.min(this.config.retryDelay*Math.pow(2,i-1),1e4);return this.logger.info("Retrying request",{endpoint:t,attempt:i,delay:u}),await this.sleep(u),this.requestWithRetry(e,t,r,i+1,!1)}throw new Error(`Network error after ${i} attempts`)}if(a.response.status===429){let u=a.response.data.error?.retryAfter||6e4;if(this.logger.warn("Rate limit exceeded",{endpoint:t,retryAfter:u}),i<this.config.retries)return await this.sleep(u),this.requestWithRetry(e,t,r,i+1,!1);throw new Error("Rate limit exceeded")}if(a.response.data?.error)throw new g(a.response.data.error)}throw this.logger.error("Request failed",o instanceof Error?o:void 0,{endpoint:t,attempt:i}),o}}sleep(e){return new Promise(t=>setTimeout(t,e))}invalidateCache(e){return this.cache.invalidate(e)}clearCache(){this.cache.clear()}getCacheStats(){return this.cache.getStats()}getQueueStatus(){return this.offlineQueue.getStatus()}async processOfflineQueue(){await this.offlineQueue.processQueue()}clearQueue(){this.offlineQueue.clear()}async healthCheck(){try{return await this.http.get("/health"),!0}catch{return!1}}},g=class extends Error{type;details;requestId;constructor(e){super(e.message||"API error"),this.name="APIClientError",this.type=e.type||"unknown_error",this.details=e.details,this.requestId=e.requestId}};var y=class extends _{server;transport;state="starting";logger;config;apiClient;startTime;shutdownTimer;activeRequests=new Map;processHandlers=[];constructor(e){super(),this.config={...e,environment:e.environment||"development",shutdownTimeout:e.shutdownTimeout||5e3},this.logger=new c("TimofiMCPClient"),this.startTime=new Date;let t={apiUrl:this.config.apiUrl,apiKey:this.config.apiKey,cacheEnabled:this.config.cacheEnabled,cacheTTL:this.config.cacheTTL,offlineQueueEnabled:this.config.offlineQueueEnabled};this.apiClient=new f(t),this.server=new I({name:e.name,version:e.version}),this.setupServerHandlers(),this.setupProcessHandlers()}setupServerHandlers(){this.server.setRequestHandler(O,async e=>{let t=this.generateRequestId(),r=this.createRequestContext(t,"initialize",e.params);this.logger.info("Initialize request",{params:e.params});try{let i={protocolVersion:"2024-11-05",capabilities:{tools:{},resources:{},prompts:{}},serverInfo:{name:this.config.name,version:this.config.version}};return this.logger.info("Initialize response",{duration:Date.now()-r.startTime}),i}catch(i){throw this.logger.error("Initialize request failed",i),i}finally{this.activeRequests.delete(t)}}),this.server.setRequestHandler(M,async()=>{let e=this.generateRequestId(),t=this.createRequestContext(e,"tools/list");this.logger.info("Tools list request");try{let r=this.getToolsList();return this.logger.info("Tools list response",{count:r.length,duration:Date.now()-t.startTime}),{tools:r}}catch(r){throw this.logger.error("Tools list request failed",r),r}finally{this.activeRequests.delete(e)}}),this.server.setRequestHandler(L,async()=>{let e=this.generateRequestId(),t=this.createRequestContext(e,"resources/list");this.logger.info("Resources list request");try{let r=[];return this.logger.info("Resources list response",{duration:Date.now()-t.startTime}),{resources:r}}catch(r){throw this.logger.error("Resources list request failed",r),r}finally{this.activeRequests.delete(e)}}),this.server.setRequestHandler(k,async()=>({status:"pong",timestamp:new Date().toISOString(),server:this.config.name,version:this.config.version})),this.server.setRequestHandler(A,async e=>{let t=this.generateRequestId(),r=this.createRequestContext(t,"tools/call",e.params);this.logger.info("Tool call request",{tool:e.params.name,args:e.params.arguments});try{let i=await this.handleToolCall(e.params.name,e.params.arguments||{});return this.logger.info("Tool call response",{tool:e.params.name,duration:Date.now()-r.startTime}),i}catch(i){throw this.logger.error("Tool call request failed",i,{tool:e.params.name}),i instanceof g?new Error(`API Error: ${i.message}`):i}finally{this.activeRequests.delete(t)}}),this.server.onerror=e=>{this.logger.error("MCP Server error",e),this.emit("error",{error:e}),this.state!=="shutting_down"&&this.setState("error")},this.logger.debug("MCP protocol handlers initialized")}setupProcessHandlers(){let e=()=>this.shutdown("SIGTERM"),t=()=>this.shutdown("SIGINT"),r=s=>{this.logger.error("Uncaught exception",s),this.shutdown("uncaught_exception")},i=s=>{this.logger.error("Unhandled rejection",s),this.shutdown("unhandled_rejection")};process.on("SIGTERM",e),process.on("SIGINT",t),process.on("uncaughtException",r),process.on("unhandledRejection",i),this.processHandlers=[{event:"SIGTERM",handler:e},{event:"SIGINT",handler:t},{event:"uncaughtException",handler:r},{event:"unhandledRejection",handler:i}]}cleanupProcessHandlers(){for(let{event:e,handler:t}of this.processHandlers)process.removeListener(e,t);this.processHandlers=[]}getToolsList(){return[{name:"store_memory",description:"Store a new memory in the context store for a repository",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"},content:{type:"string",description:"Content to remember"},context:{type:"object",description:"Additional context information"},metadata:{type:"object",description:"Additional metadata"}},required:["repository","content"]}},{name:"search_memories",description:"Search for memories related to a query",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"},query:{type:"string",description:"Search query"},limit:{type:"number",description:"Maximum number of results"},threshold:{type:"number",description:"Similarity threshold (0-1)"}},required:["repository","query"]}},{name:"search_code",description:"Search for code patterns across repository files",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"},query:{type:"string",description:"Search query"},language:{type:"string",description:"Filter by programming language"},path:{type:"string",description:"Filter by file path pattern"},limit:{type:"number",description:"Maximum number of results"}},required:["repository","query"]}},{name:"get_file_context",description:"Get the context of a specific file with symbols and structure",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"},filePath:{type:"string",description:"Path to the file"},lineStart:{type:"number",description:"Starting line number"},lineEnd:{type:"number",description:"Ending line number"}},required:["repository","filePath"]}},{name:"get_project_overview",description:"Get project overview: file tree structure, README, and key docs (CONTRIBUTING, LICENSE, etc.). Use this to understand the repository layout and documentation before searching code.",inputSchema:{type:"object",properties:{repository:{type:"string",description:"Repository identifier (owner/repo)"}},required:["repository"]}},{name:"list_repositories",description:"List all available repositories that have been connected and indexed",inputSchema:{type:"object",properties:{}}},{name:"start_indexing",description:"Start indexing a repository",inputSchema:{type:"object",properties:{owner:{type:"string",description:"Repository owner"},repo:{type:"string",description:"Repository name"},incremental:{type:"boolean",description:"Perform incremental indexing"},branch:{type:"string",description:"Branch to index"}},required:["owner","repo"]}},{name:"get_indexing_status",description:"Get the status of a repository indexing task",inputSchema:{type:"object",properties:{taskId:{type:"string",description:"Indexing task ID"}},required:["taskId"]}}]}async handleToolCall(e,t){switch(e){case"store_memory":return{content:[{type:"text",text:`Memory stored successfully with ID: ${(await this.apiClient.storeMemory({repository:t.repository,content:t.content,context:t.context,metadata:t.metadata})).memoryId}`}]};case"search_memories":let i=await this.apiClient.searchMemories({repository:t.repository,query:t.query,limit:t.limit,threshold:t.threshold});return{content:[{type:"text",text:JSON.stringify(i,null,2)}]};case"search_code":let s=await this.apiClient.searchCode({repository:t.repository,query:t.query,language:t.language,path:t.path,limit:t.limit});return{content:[{type:"text",text:JSON.stringify(s,null,2)}]};case"get_file_context":let o=await this.apiClient.getFileContext({repository:t.repository,filePath:t.filePath,lineStart:t.lineStart,lineEnd:t.lineEnd});return{content:[{type:"text",text:JSON.stringify(o,null,2)}]};case"get_project_overview":let a=await this.apiClient.getProjectOverview({repository:t.repository});return{content:[{type:"text",text:JSON.stringify(a,null,2)}]};case"list_repositories":let u=await this.apiClient.listRepositories();return{content:[{type:"text",text:JSON.stringify(u,null,2)}]};case"start_indexing":let p=await this.apiClient.startIndexing({owner:t.owner,repo:t.repo,incremental:t.incremental,branch:t.branch});return{content:[{type:"text",text:`Indexing started. Task ID: ${p.taskId}, Status: ${p.status}`}]};case"get_indexing_status":let v=await this.apiClient.getIndexingTask({taskId:t.taskId});return{content:[{type:"text",text:JSON.stringify(v,null,2)}]};default:throw new Error(`Unknown tool: ${e}`)}}generateRequestId(){return Q()}createRequestContext(e,t,r){let i={requestId:e,timestamp:new Date,method:t,params:r,startTime:Date.now()};return this.activeRequests.set(e,i),i}setState(e){let t=this.state;this.state=e,this.logger.info("State change",{from:t,to:e})}async start(){try{if(this.setState("starting"),this.logger.info("Starting Timofi MCP Client",{name:this.config.name,version:this.config.version,apiUrl:this.config.apiUrl}),!await this.apiClient.healthCheck())throw new Error("API server is not accessible");this.transport=new E,await this.server.connect(this.transport),this.setState("ready"),this.logger.info("Timofi MCP Client started successfully"),this.emit("started",{config:this.config}),this.emit("ready",{uptime:this.getUptime()})}catch(e){throw this.setState("error"),this.logger.error("Failed to start MCP Client",e),this.emit("error",{error:e}),e}}async shutdown(e="manual"){if(this.state==="shutting_down"||this.state==="stopped")return;this.setState("shutting_down"),this.logger.info(`Shutting down client (reason: ${e})`);let t=new Promise(r=>{this.shutdownTimer=setTimeout(()=>{this.logger.warn("Forced shutdown after timeout"),r()},this.config.shutdownTimeout)});try{await Promise.race([this.waitForActiveRequests(),t]),await this.apiClient.processOfflineQueue(),this.server&&this.transport&&await this.server.close(),this.cleanupProcessHandlers(),this.setState("stopped"),this.logger.info("Client shutdown completed"),this.emit("shutdown",{reason:e})}catch(r){this.logger.error("Error during shutdown",r),this.setState("error"),this.emit("error",{error:r})}finally{this.shutdownTimer&&clearTimeout(this.shutdownTimer)}}async waitForActiveRequests(){let r=0;for(;this.activeRequests.size>0&&r<3e3;)await new Promise(i=>setTimeout(i,100)),r+=100;this.activeRequests.size>0&&this.logger.warn(`${this.activeRequests.size} requests still active during shutdown`)}getState(){return this.state}getUptime(){return Date.now()-this.startTime.getTime()}getConfig(){return{...this.config}}getActiveRequestsCount(){return this.activeRequests.size}getCacheStats(){return this.apiClient.getCacheStats()}getQueueStatus(){return this.apiClient.getQueueStatus()}invalidateCache(e){return this.apiClient.invalidateCache(e)}};import*as T from"dotenv";T.config();var h=new c("MCPClientCLI"),z=["TIMOFI_API_URL","TIMOFI_API_KEY"],w=z.filter(n=>!process.env[n]);w.length>0&&(h.error("Missing required environment variables",void 0,{missing:w}),console.error(`
3
+ Missing required environment variables: ${w.join(", ")}
4
+ `),console.error("Please configure your .env file with:"),console.error(" TIMOFI_API_URL=http://localhost:3000"),console.error(` TIMOFI_API_KEY=your-api-key
5
+ `),process.exit(1));async function $(){try{let n={name:"timofi-context-server",version:process.env.npm_package_version||"1.0.0",apiUrl:process.env.TIMOFI_API_URL,apiKey:process.env.TIMOFI_API_KEY,environment:process.env.NODE_ENV||"development",cacheEnabled:process.env.CACHE_ENABLED!=="false",cacheTTL:parseInt(process.env.CACHE_TTL||"300000"),offlineQueueEnabled:process.env.OFFLINE_QUEUE_ENABLED!=="false"};h.info("Initializing Timofi MCP Client",{name:n.name,version:n.version,apiUrl:n.apiUrl,cacheEnabled:n.cacheEnabled,offlineQueueEnabled:n.offlineQueueEnabled});let e=new y(n);e.on("started",()=>{h.info("MCP Client started successfully")}),e.on("ready",({uptime:t})=>{h.info("MCP Client ready",{uptime:t})}),e.on("error",({error:t})=>{h.error("MCP Client error",t)}),e.on("shutdown",({reason:t})=>{h.info("MCP Client shutdown",{reason:t})}),await e.start(),h.info("Timofi MCP Client is running")}catch(n){h.error("Failed to start MCP Client",n),process.exit(1)}}$().catch(n=>{h.error("Unhandled error in main",n),process.exit(1)});
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@timofi/context-server",
3
+ "version": "1.0.0",
4
+ "description": "MCP client for Timofi Context Server - Repository-aware memory and code intelligence for Claude Desktop",
5
+ "type": "module",
6
+ "bin": {
7
+ "timofi-mcp-client": "dist/mcp-client-cli.js"
8
+ },
9
+ "main": "dist/index.js",
10
+ "files": [
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=22.0.0",
16
+ "npm": ">=10.0.0"
17
+ },
18
+ "dependencies": {
19
+ "@modelcontextprotocol/sdk": "^0.4.0",
20
+ "axios": "^1.6.2",
21
+ "dotenv": "^16.3.1",
22
+ "uuid": "^9.0.1",
23
+ "winston": "^3.11.0"
24
+ },
25
+ "license": "MIT",
26
+ "author": "GoldCode.io <team@goldcode.io>",
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "memory",
31
+ "code-intelligence",
32
+ "github",
33
+ "repository",
34
+ "ai-assistant",
35
+ "claude-code",
36
+ "sourcegraph",
37
+ "vector-database"
38
+ ],
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "https://github.com/goldcode-io/timofi-context-server.git"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }