@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 +572 -0
- package/dist/index.js +1 -0
- package/dist/mcp-client-cli.js +5 -0
- package/package.json +46 -0
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
|
+
}
|