Skip to content

Commit b0b608b

Browse files
Initial commit: ConextForge Memory MCP v0
- FastAPI-based memory service with store, search, and embed APIs - Versioned API at /v0/ with deterministic hash-based embeddings - JSONL storage backend with namespace/project filtering - Python and TypeScript client implementations - OpenAPI specification and CI workflow - Comprehensive smoke tests passing
0 parents  commit b0b608b

File tree

13 files changed

+522
-0
lines changed

13 files changed

+522
-0
lines changed

.editorconfig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
root = true
2+
3+
[*]
4+
end_of_line = lf
5+
insert_final_newline = true
6+
charset = utf-8
7+
indent_style = space
8+
indent_size = 2
9+
trim_trailing_whitespace = true
10+
11+
[*.{py,rs,go,ts,tsx}]
12+
indent_size = 4
13+
14+
[*.md]
15+
max_line_length = off
16+
trim_trailing_whitespace = false
17+
18+

.github/workflows/ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: ci
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
test:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v4
12+
- uses: actions/setup-python@v5
13+
with:
14+
python-version: '3.11'
15+
- name: Install
16+
run: |
17+
python -m pip install --upgrade pip
18+
pip install -r requirements.txt
19+
- name: Lint
20+
run: |
21+
ruff check src || true
22+
- name: Import smoke
23+
run: |
24+
python - << 'PY'
25+
import importlib
26+
import sys
27+
sys.path.append('src')
28+
import contextforge_memory
29+
from contextforge_memory.main import app
30+
print('ok')
31+
PY
32+
33+

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
__pycache__/
2+
.venv/
3+
.env
4+
.env.*
5+
.DS_Store
6+
.pytest_cache/
7+
.mypy_cache/
8+
dist/
9+
build/
10+
.coverage
11+
coverage.xml
12+
.idea/
13+
.vscode/
14+
node_modules/
15+
logs/
16+
data/
17+
18+

LICENSE

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
MIT License
2+
3+
Copyright (c) 2025 bofh
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+
23+

Makefile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
run:
2+
uvicorn src.contextforge_memory.main:app --host 0.0.0.0 --port 8085
3+
4+
install:
5+
python -m pip install -r requirements.txt
6+
7+
format:
8+
python -m pip install black && black src clients || true
9+
10+
lint:
11+
python -m pip install ruff && ruff check src || true
12+
13+
smoke:
14+
python -c "import contextforge_memory; from contextforge_memory.main import app; print('ok')"
15+
16+

README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## ContextForge Memory (v0)
2+
3+
Lightweight external memory layer providing store, search, and embed APIs for multi-repo reuse (Brainstorm, Cursor, and others).
4+
5+
### Features
6+
- Versioned API: `/v0/` with `store`, `search`, `embed`.
7+
- Deterministic, minimal embedding (hash-based) for baseline functionality.
8+
- Pluggable storage (default: JSONL on disk).
9+
10+
### Quickstart
11+
```bash
12+
python -m venv .venv && source .venv/bin/activate
13+
pip install -r requirements.txt
14+
uvicorn src.contextforge_memory.main:app --host 0.0.0.0 --port 8085
15+
```
16+
17+
### API Endpoints
18+
- `GET /v0/health`
19+
- `POST /v0/store`
20+
- `POST /v0/search`
21+
- `POST /v0/embed`
22+
23+
See `openapi/openapi-v0.yaml` for schemas.
24+
25+
### Clients
26+
- Python: `clients/python/contextforge_client.py`
27+
- TypeScript: `clients/typescript/contextforgeClient.ts`
28+
29+
### CI
30+
Basic import smoke in GitHub Actions (see `.github/workflows/ci.yml`).
31+
32+
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from __future__ import annotations
2+
3+
from typing import Any, Dict, List
4+
import httpx
5+
6+
7+
class ContextForgeClient:
8+
def __init__(self, base_url: str) -> None:
9+
self.base_url = base_url.rstrip("/")
10+
self._client = httpx.Client(timeout=15.0)
11+
12+
def health(self) -> Dict[str, Any]:
13+
r = self._client.get(f"{self.base_url}/v0/health")
14+
r.raise_for_status()
15+
return r.json()
16+
17+
def store(self, items: List[Dict[str, Any]]) -> Dict[str, Any]:
18+
r = self._client.post(f"{self.base_url}/v0/store", json={"items": items})
19+
r.raise_for_status()
20+
return r.json()
21+
22+
def search(self, namespace: str, project_id: str, query: str, top_k: int = 5) -> Dict[str, Any]:
23+
payload = {
24+
"namespace": namespace,
25+
"project_id": project_id,
26+
"query": query,
27+
"top_k": top_k,
28+
}
29+
r = self._client.post(f"{self.base_url}/v0/search", json=payload)
30+
r.raise_for_status()
31+
return r.json()
32+
33+
def embed(self, texts: List[str]) -> Dict[str, Any]:
34+
r = self._client.post(f"{self.base_url}/v0/embed", json={"texts": texts})
35+
r.raise_for_status()
36+
return r.json()
37+
38+
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
export type MemoryItem = {
2+
id: string;
3+
namespace: string;
4+
project_id: string;
5+
kind: string;
6+
text: string;
7+
metadata?: Record<string, unknown>;
8+
};
9+
10+
export class ContextForgeClient {
11+
private baseUrl: string;
12+
13+
constructor(baseUrl: string) {
14+
this.baseUrl = baseUrl.replace(/\/$/, "");
15+
}
16+
17+
async health(): Promise<{ status: string }> {
18+
const r = await fetch(`${this.baseUrl}/v0/health`);
19+
if (!r.ok) throw new Error(`health failed: ${r.status}`);
20+
return r.json();
21+
}
22+
23+
async store(items: MemoryItem[]): Promise<{ stored: number }> {
24+
const r = await fetch(`${this.baseUrl}/v0/store`, {
25+
method: "POST",
26+
headers: { "Content-Type": "application/json" },
27+
body: JSON.stringify({ items }),
28+
});
29+
if (!r.ok) throw new Error(`store failed: ${r.status}`);
30+
return r.json();
31+
}
32+
33+
async search(namespace: string, project_id: string, query: string, top_k = 5): Promise<{ results: MemoryItem[] }> {
34+
const r = await fetch(`${this.baseUrl}/v0/search`, {
35+
method: "POST",
36+
headers: { "Content-Type": "application/json" },
37+
body: JSON.stringify({ namespace, project_id, query, top_k }),
38+
});
39+
if (!r.ok) throw new Error(`search failed: ${r.status}`);
40+
return r.json();
41+
}
42+
43+
async embed(texts: string[]): Promise<{ vectors: number[][] }> {
44+
const r = await fetch(`${this.baseUrl}/v0/embed`, {
45+
method: "POST",
46+
headers: { "Content-Type": "application/json" },
47+
body: JSON.stringify({ texts }),
48+
});
49+
if (!r.ok) throw new Error(`embed failed: ${r.status}`);
50+
return r.json();
51+
}
52+
}
53+
54+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
### ADR-0001: Product Scope and API Contracts (ContextForge Memory)
2+
3+
Status: Proposed
4+
5+
#### Problem
6+
We need a small, reusable memory layer usable by Brainstorm and Cursor. It must be simple, versioned, and easy to self-host.
7+
8+
#### Decision
9+
- Provide `/v0/health`, `/v0/store`, `/v0/search`, `/v0/embed`.
10+
- Use deterministic, hash-based embeddings initially; allow plug-in providers later.
11+
- Persist JSONL to disk by default; support alternative backends via env switches.
12+
- Namespacing: `namespace = vendor:project_id:env`; requests must include `namespace` and `project_id`.
13+
14+
#### Consequences
15+
Positive: trivial to run, API-first, stable contract, multi-repo reuse.
16+
Negative: baseline embeddings are weak semantically; acceptable for phase 1.
17+
18+
#### Versioning
19+
- Semantic version in OpenAPI; breaking changes bump path to `/v1`.
20+
21+
#### Security & Limits
22+
- Start unauthenticated on localhost; add API key header later.
23+
- Soft limits: max text 64KB, batch store up to 100 items.
24+
25+
#### Testing & Conformance
26+
- Contract tests run as part of CI; consumer repos can import and run the same tests.
27+
28+

0 commit comments

Comments
 (0)