Skip to content

Commit d41a755

Browse files
committed
v0.5.0: Knowledge Graph with Ebbinghaus memory decay + Native MCP client
Killer features for virality: Knowledge Graph (internal/knowledge/): - Entity storage with types: person, place, org, concept, preference, project, event, fact - Weighted relationships between entities (directed graph) - Ebbinghaus forgetting curve: S(t) = S₀ × e^(-λt) with spacing effect - Automatic pruning of decayed memories below threshold - Context injection into system prompt (strongest memories) - Full CRUD API endpoints + Web UI panel with search - 9/9 tests passing Native MCP Client (internal/mcp/): - Model Context Protocol client supporting stdio and SSE transports - JSON-RPC 2.0 implementation (zero external dependencies) - Tool discovery and invocation across connected servers - Auto-reconnect from persisted server configurations - Web UI panel for connection management - 13/13 tests passing Agent Integration: - Knowledge graph skills: add, relate, query, relations, delete, stats - MCP skills: connect, disconnect, list, call - System prompt includes knowledge graph context + MCP tool listings - Auto-connect configured MCP servers on startup Web UI: - Knowledge Graph panel with entity browser, search, stats, relation viewer - MCP Servers panel with connection form, server list, tool inventory - Keyboard shortcuts: Ctrl+Shift+G (Knowledge), Ctrl+Shift+M (MCP) - Welcome screen updated with new shortcuts README updated with new features, architecture diagram, API endpoints. Binary size: 11MB (unchanged). All 132 tests passing.
1 parent f323de6 commit d41a755

15 files changed

Lines changed: 2854 additions & 120 deletions

File tree

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# PennyClaw Makefile
2-
# Version: 0.4.1
2+
# Version: 0.5.0
33

4-
VERSION ?= 0.4.1
4+
VERSION ?= 0.5.0
55
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "dev")
66
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
77
LDFLAGS = -s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.buildDate=$(DATE)

README.md

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,16 @@ PennyClaw ships with a comprehensive set of tools the agent can invoke:
108108
| **Notes** | `note_delete` | Remove notes |
109109
| **Notes** | `note_search` | Full-text search with snippet extraction |
110110
| **Email** | `send_email` | Send email notifications via SMTP |
111+
| **Knowledge** | `knowledge_add` | Add entities (people, places, concepts) to the knowledge graph |
112+
| **Knowledge** | `knowledge_relate` | Create relationships between entities |
113+
| **Knowledge** | `knowledge_query` | Search the knowledge graph by name |
114+
| **Knowledge** | `knowledge_relations` | Get all relationships for an entity |
115+
| **Knowledge** | `knowledge_delete` | Remove an entity and its relations |
116+
| **Knowledge** | `knowledge_stats` | Get knowledge graph statistics |
117+
| **MCP** | `mcp_connect` | Connect to an MCP server (stdio or SSE transport) |
118+
| **MCP** | `mcp_disconnect` | Disconnect from an MCP server |
119+
| **MCP** | `mcp_list` | List connected MCP servers and their tools |
120+
| **MCP** | `mcp_call` | Call a tool on a connected MCP server |
111121

112122
Skills can also be loaded from external YAML/JSON bundles via the skill pack system, allowing you to extend PennyClaw without modifying Go code.
113123

@@ -135,6 +145,26 @@ The embedded web interface includes:
135145
- Notification sound when responses arrive
136146
- Keyboard shortcuts: Ctrl+K (new chat), Ctrl+L (clear), Ctrl+E (export), Esc (close panels)
137147

148+
### Knowledge Graph with Memory Decay
149+
150+
PennyClaw maintains a knowledge graph that learns about your world over time. Entities (people, places, concepts, preferences, projects) are stored with weighted relationships and subject to **Ebbinghaus memory decay** — memories that are never reinforced gradually fade, keeping the graph lean and relevant.
151+
152+
- **Automatic context injection** — The strongest memories are included in every system prompt, giving the agent persistent awareness
153+
- **Ebbinghaus forgetting curve** — Strength decays exponentially over time: `S(t) = S₀ × e^(-λt)`
154+
- **Spacing effect** — Frequently accessed memories decay slower (adjusted by access count)
155+
- **Automatic pruning** — Entities below the strength threshold are removed to save memory
156+
- **Web UI panel** — Browse, search, and manage entities and relationships visually
157+
158+
### Native MCP Client
159+
160+
PennyClaw includes a built-in [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) client, allowing it to connect to any MCP-compatible server and use its tools. This gives PennyClaw instant access to hundreds of community-built integrations.
161+
162+
- **Stdio transport** — Launch MCP servers as subprocesses (e.g., `npx @modelcontextprotocol/server-filesystem`)
163+
- **SSE transport** — Connect to remote MCP servers via HTTP Server-Sent Events
164+
- **Auto-reconnect** — Server configurations are persisted and reconnected on restart
165+
- **Web UI panel** — Add, manage, and monitor MCP server connections visually
166+
- **Zero dependencies** — Pure Go JSON-RPC 2.0 implementation
167+
138168
### Automation
139169

140170
- **Cron scheduler** — Schedule recurring tasks with cron expressions (e.g., daily summaries, periodic checks)
@@ -193,10 +223,12 @@ graph TD
193223
LLM["LLM Gateway<br/>OpenAI / Anthropic / Gemini / OpenRouter"]
194224
SKILLS["Skills Registry<br/>shell / files / web / tasks / notes / email"]
195225
SKILLPACK["Skill Packs<br/>YAML/JSON bundles"]
226+
KG["Knowledge Graph<br/>Ebbinghaus memory decay"]
227+
MCP["MCP Client<br/>stdio + SSE transports"]
196228
end
197229
198230
subgraph Storage["Storage"]
199-
SQLITE["SQLite<br/>conversations + memory"]
231+
SQLITE["SQLite<br/>conversations + memory + knowledge"]
200232
TASKS["JSON<br/>task store"]
201233
NOTES["Markdown<br/>knowledge base"]
202234
WORKSPACE["Workspace<br/>persistent files"]
@@ -224,6 +256,9 @@ graph TD
224256
AGENT --> SQLITE
225257
AGENT --> WORKSPACE
226258
AGENT --> CRON
259+
AGENT --> KG
260+
AGENT --> MCP
261+
KG --> SQLITE
227262
VALIDATE -.-> AGENT
228263
229264
style Channels fill:#064e3b,stroke:#34d399,stroke-width:2px,color:#34d399
@@ -246,8 +281,10 @@ internal/
246281
config/ Config loading, env var resolution, validation
247282
cron/ Cron scheduler for recurring tasks
248283
health/ Health checks, system metrics, Prometheus endpoint
284+
knowledge/ Knowledge graph with Ebbinghaus memory decay (SQLite)
249285
llm/ Multi-provider LLM gateway (OpenAI, Anthropic, Gemini)
250286
logging/ Structured leveled logger (JSON or human-readable)
287+
mcp/ Model Context Protocol client (stdio + SSE transports)
251288
memory/ SQLite-backed conversation store
252289
notify/ Email notifications via SMTP
253290
sandbox/ Linux namespace/cgroup sandboxing for tool execution
@@ -262,8 +299,8 @@ docs/ Deploy tutorial, assets
262299

263300
1. A message arrives via one of the channels (web UI, Telegram, or webhook).
264301
2. The agent saves the message to SQLite and builds a context window from conversation history.
265-
3. The system prompt is assembled from the base prompt plus workspace context.
266-
4. The LLM is called with the message history and available tools.
302+
3. The system prompt is assembled from the base prompt, workspace context, knowledge graph memories, and MCP tool listings.
303+
4. The LLM is called with the message history and available tools (including MCP tools from connected servers).
267304
5. If the LLM returns tool calls, the agent executes each skill and feeds results back.
268305
6. Steps 4-5 repeat (up to 10 iterations) until the LLM returns a text response.
269306
7. The response is saved to memory and returned to the channel.
@@ -438,6 +475,15 @@ config validation failed:
438475
| `PUT` | `/api/config` | Update configuration |
439476
| `POST` | `/api/webhooks` | Webhook endpoint (when enabled) |
440477
| `POST` | `/api/upload` | File upload |
478+
| `GET` | `/api/knowledge` | List knowledge graph entities |
479+
| `GET` | `/api/knowledge/search?q=` | Search entities by name |
480+
| `GET` | `/api/knowledge/stats` | Knowledge graph statistics |
481+
| `GET` | `/api/knowledge/:id/relations` | Get entity relationships |
482+
| `DELETE` | `/api/knowledge/:id` | Delete an entity |
483+
| `GET` | `/api/mcp` | List MCP connections and tools |
484+
| `POST` | `/api/mcp/connect` | Connect to an MCP server |
485+
| `POST` | `/api/mcp/disconnect` | Disconnect from an MCP server |
486+
| `GET` | `/api/mcp/tools` | List all available MCP tools |
441487

442488
## Security
443489

cmd/pennyclaw/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ func main() {
8787

8888
// Start web server
8989
srv := web.NewServer(cfg.Server.Host, cfg.Server.Port, ag.HandleMessage, cfg, *configPath,
90-
ag.Memory(), version, ag.Workspace(), ag.Scheduler(), ag.SkillPack(), webhookHandler, ag.Health(), ag.TaskStore(), ag.NoteStore())
90+
ag.Memory(), version, ag.Workspace(), ag.Scheduler(), ag.SkillPack(), webhookHandler, ag.Health(), ag.TaskStore(), ag.NoteStore(), ag.Graph(), ag.MCPManager())
9191
go func() {
9292
log.Printf("PennyClaw %s starting on %s:%d", version, cfg.Server.Host, cfg.Server.Port)
9393
if err := srv.Start(); err != nil {

internal/agent/agent.go

Lines changed: 83 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313
"github.com/mandarl/pennyclaw/internal/config"
1414
"github.com/mandarl/pennyclaw/internal/cron"
1515
"github.com/mandarl/pennyclaw/internal/health"
16+
"github.com/mandarl/pennyclaw/internal/knowledge"
1617
"github.com/mandarl/pennyclaw/internal/llm"
18+
"github.com/mandarl/pennyclaw/internal/mcp"
1719
"github.com/mandarl/pennyclaw/internal/memory"
1820
"github.com/mandarl/pennyclaw/internal/notify"
1921
"github.com/mandarl/pennyclaw/internal/sandbox"
@@ -38,6 +40,8 @@ type Agent struct {
3840
health *health.Checker
3941
taskStore *skills.TaskStore
4042
noteStore *skills.NoteStore
43+
graph *knowledge.Graph
44+
mcpMgr *mcp.Manager
4145
// supportsTools indicates whether the LLM provider supports tool/function calling.
4246
supportsTools bool
4347
}
@@ -104,6 +108,19 @@ func New(cfg *config.Config, dataDir string) (*Agent, error) {
104108
// Register productivity skills (tasks, notes)
105109
ts, ns := skills.RegisterProductivitySkills(skillRegistry, dataDir)
106110

111+
// Initialize knowledge graph
112+
kg, err := knowledge.NewGraph(mem.DB())
113+
if err != nil {
114+
log.Printf("Warning: failed to initialize knowledge graph: %v", err)
115+
}
116+
if kg != nil {
117+
log.Printf("Knowledge graph initialized")
118+
}
119+
120+
// Initialize MCP manager
121+
mcpManager := mcp.NewManager(dataDir)
122+
log.Printf("MCP client manager initialized")
123+
107124
// Initialize health checker
108125
hc := health.NewChecker(Version, provider.Name(), cfg.LLM.Model, len(skillRegistry.AsTools()))
109126

@@ -118,6 +135,8 @@ func New(cfg *config.Config, dataDir string) (*Agent, error) {
118135
health: hc,
119136
taskStore: ts,
120137
noteStore: ns,
138+
graph: kg,
139+
mcpMgr: mcpManager,
121140
supportsTools: supportsTools,
122141
}
123142

@@ -152,6 +171,28 @@ func New(cfg *config.Config, dataDir string) (*Agent, error) {
152171
// Register cron skills
153172
agent.registerCronSkills()
154173

174+
// Register knowledge graph skills
175+
agent.registerKnowledgeSkills()
176+
177+
// Register MCP skills
178+
agent.registerMCPSkills()
179+
180+
// Auto-connect configured MCP servers
181+
go func() {
182+
configs, err := mcpManager.LoadConfigs()
183+
if err != nil {
184+
log.Printf("Warning: failed to load MCP configs: %v", err)
185+
return
186+
}
187+
for _, cfg := range configs {
188+
if cfg.Enabled {
189+
if err := mcpManager.Connect(context.Background(), cfg); err != nil {
190+
log.Printf("Warning: failed to connect MCP server %s: %v", cfg.Name, err)
191+
}
192+
}
193+
}
194+
}()
195+
155196
// Update skill count now that all skills are registered
156197
hc.UpdateSkillCount(len(skillRegistry.AsTools()))
157198

@@ -443,12 +484,26 @@ func (a *Agent) handleMessage(ctx context.Context, sessionID, userMessage, chann
443484

444485
// If no tool calls, return the text response
445486
if len(resp.ToolCalls) == 0 {
487+
content := resp.Content
488+
// Guard against empty responses — the LLM sometimes returns
489+
// empty content when it's unsure how to proceed
490+
if content == "" {
491+
if i > 0 {
492+
// We executed tools but got no summary — ask the LLM to summarize
493+
messages = append(messages, llm.Message{
494+
Role: "user",
495+
Content: "Please summarize what you just did and provide a helpful response to the user.",
496+
})
497+
continue
498+
}
499+
content = "I'm not sure how to help with that. Could you rephrase your request?"
500+
}
446501
// Save assistant response
447-
if err := a.memory.SaveMessage(sessionID, "assistant", resp.Content, channel); err != nil {
502+
if err := a.memory.SaveMessage(sessionID, "assistant", content, channel); err != nil {
448503
log.Printf("Warning: failed to save response: %v", err)
449504
}
450505
a.health.RecordRequest(time.Since(start), nil)
451-
return resp.Content, nil
506+
return content, nil
452507
}
453508

454509
// Execute tool calls
@@ -516,6 +571,22 @@ func (a *Agent) buildSystemPrompt() string {
516571
prompt += "\n\n" + skillpackContext
517572
}
518573

574+
// Add knowledge graph context
575+
if a.graph != nil {
576+
kgContext := a.graph.GetContext(20)
577+
if kgContext != "" {
578+
prompt += "\n\n--- Knowledge Graph (things I remember) ---\n" + kgContext
579+
}
580+
}
581+
582+
// Add MCP tools context
583+
if a.mcpMgr != nil {
584+
mcpTools := a.mcpMgr.Tools()
585+
if len(mcpTools) > 0 {
586+
prompt += fmt.Sprintf("\n\n--- MCP Tools (%d available from external servers) ---", len(mcpTools))
587+
}
588+
}
589+
519590
return prompt
520591
}
521592

@@ -564,6 +635,16 @@ func (a *Agent) NoteStore() *skills.NoteStore {
564635
return a.noteStore
565636
}
566637

638+
// Graph returns the agent's knowledge graph.
639+
func (a *Agent) Graph() *knowledge.Graph {
640+
return a.graph
641+
}
642+
643+
// MCPManager returns the agent's MCP connection manager.
644+
func (a *Agent) MCPManager() *mcp.Manager {
645+
return a.mcpMgr
646+
}
647+
567648
// HealthCheck returns the agent's health status.
568649
func (a *Agent) HealthCheck() map[string]interface{} {
569650
return map[string]interface{}{

0 commit comments

Comments
 (0)