Skip to content

Commit 0d46bf6

Browse files
aksOpsclaude
andcommitted
feat: initial implementation of pure-Go GraphRAG MCP server
- 5-phase GraphRAG pipeline: load, chunk, embed, extract graph, community detection - Three LLM providers: Azure OpenAI, Ollama, HuggingFace (TGI) - Pure Go SQLite store (modernc.org/sqlite, CGO_ENABLED=0) - Louvain community detection with hierarchical levels - 12 MCP tools via SSE transport (mark3labs/mcp-go) - REST API + embedded vanilla-JS SPA with vis-network graph - Concurrent batched embeddings, parallel graph extraction, parallel community summarization - CLI: index, stats, serve (cobra + viper) - GitHub Actions: CI, auto-beta, release + GoReleaser cross-platform builds Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0 parents  commit 0d46bf6

44 files changed

Lines changed: 5349 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/beta.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: Auto Beta
2+
3+
on:
4+
push:
5+
branches: [main]
6+
7+
permissions:
8+
contents: write
9+
10+
jobs:
11+
auto-beta:
12+
uses: RandomCodeSpace/central-ops/.github/workflows/reusable-auto-beta.yml@main
13+
secrets: inherit

.github/workflows/ci.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
build-and-test:
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v4
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version-file: go.mod
23+
cache: true
24+
25+
- name: Build
26+
run: CGO_ENABLED=0 go build ./...
27+
28+
- name: Vet
29+
run: go vet ./...
30+
31+
- name: Test
32+
run: CGO_ENABLED=0 go test -race -timeout 120s ./...

.github/workflows/release.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
version:
7+
description: 'Base beta version to release (e.g., v0.0.5-beta)'
8+
required: true
9+
10+
permissions:
11+
contents: write
12+
13+
jobs:
14+
release:
15+
uses: RandomCodeSpace/central-ops/.github/workflows/reusable-release.yml@main
16+
secrets: inherit
17+
with:
18+
version: ${{ inputs.version }}

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Binaries
2+
docsgraph
3+
docsgraph.exe
4+
docsgraph-linux
5+
docsgraph-*
6+
7+
# Data
8+
*.db
9+
*.db-shm
10+
*.db-wal
11+
12+
# Build artifacts
13+
dist/
14+
15+
# OS
16+
.DS_Store
17+
Thumbs.db

.goreleaser.yaml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
version: 2
2+
3+
project_name: docsgraph
4+
5+
before:
6+
hooks:
7+
- go mod tidy
8+
9+
builds:
10+
- id: docsgraph
11+
main: .
12+
binary: docsgraph
13+
env:
14+
- CGO_ENABLED=0
15+
goos:
16+
- linux
17+
- windows
18+
- darwin
19+
goarch:
20+
- amd64
21+
- arm64
22+
ldflags:
23+
- -s -w
24+
- -X main.version={{.Version}}
25+
- -X main.commit={{.Commit}}
26+
- -X main.date={{.Date}}
27+
28+
archives:
29+
- id: docsgraph
30+
builds: [docsgraph]
31+
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
32+
format_overrides:
33+
- goos: windows
34+
format: zip
35+
files:
36+
- config.example.yaml
37+
- LICENSE*
38+
- README*
39+
40+
checksum:
41+
name_template: "checksums.txt"
42+
algorithm: sha256
43+
44+
release:
45+
github:
46+
owner: RandomCodeSpace
47+
name: docsgraphcontext
48+
draft: false
49+
prerelease: auto
50+
name_template: "{{.ProjectName}} {{.Tag}}"
51+
52+
changelog:
53+
sort: asc
54+
use: github
55+
filters:
56+
exclude:
57+
- "^docs:"
58+
- "^test:"
59+
- "^chore:"
60+
- Merge pull request
61+
- Merge branch

cmd/index.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/amit/docsgraphcontext/internal/llm"
8+
"github.com/amit/docsgraphcontext/internal/pipeline"
9+
"github.com/amit/docsgraphcontext/internal/store"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var (
14+
indexForce bool
15+
indexWorkers int
16+
indexBatch int
17+
indexFinalize bool
18+
indexVerbose bool
19+
)
20+
21+
var indexCmd = &cobra.Command{
22+
Use: "index [path]",
23+
Short: "Index documents (Phases 1-2: load, chunk, embed, extract graph)",
24+
Args: cobra.MaximumNArgs(1),
25+
RunE: func(cmd *cobra.Command, args []string) error {
26+
st, err := store.Open(cfg.DBPath())
27+
if err != nil {
28+
return fmt.Errorf("open store: %w", err)
29+
}
30+
defer st.Close()
31+
32+
if indexFinalize {
33+
prov, err := llm.NewProvider(&cfg.LLM)
34+
if err != nil {
35+
return fmt.Errorf("llm provider: %w", err)
36+
}
37+
pl := pipeline.New(st, prov, cfg)
38+
fmt.Fprintln(os.Stderr, "Running Phase 3-4: community detection + summaries...")
39+
if err := pl.Finalize(cmd.Context(), indexVerbose); err != nil {
40+
return err
41+
}
42+
fmt.Fprintln(os.Stderr, "Finalization complete.")
43+
return nil
44+
}
45+
46+
if len(args) == 0 {
47+
return fmt.Errorf("path required (or use --finalize)")
48+
}
49+
50+
prov, err := llm.NewProvider(&cfg.LLM)
51+
if err != nil {
52+
return fmt.Errorf("llm provider: %w", err)
53+
}
54+
55+
pl := pipeline.New(st, prov, cfg)
56+
opts := pipeline.IndexOptions{
57+
Force: indexForce,
58+
Workers: indexWorkers,
59+
Verbose: indexVerbose,
60+
}
61+
62+
fmt.Fprintf(os.Stderr, "Indexing %s (workers=%d)...\n", args[0], indexWorkers)
63+
if err := pl.IndexPath(cmd.Context(), args[0], opts); err != nil {
64+
return err
65+
}
66+
fmt.Fprintln(os.Stderr, "Indexing complete.")
67+
return nil
68+
},
69+
}
70+
71+
func init() {
72+
rootCmd.AddCommand(indexCmd)
73+
indexCmd.Flags().BoolVar(&indexForce, "force", false, "Re-index even if file hash already exists")
74+
indexCmd.Flags().IntVar(&indexWorkers, "workers", 4, "Parallel workers for indexing")
75+
indexCmd.Flags().IntVar(&indexBatch, "batch-size", 20, "Embedding batch size")
76+
indexCmd.Flags().BoolVar(&indexFinalize, "finalize", false, "Run community detection + summaries (Phases 3-4)")
77+
indexCmd.Flags().BoolVar(&indexVerbose, "verbose", false, "Show per-file errors")
78+
}

cmd/root.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/amit/docsgraphcontext/internal/config"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var (
12+
cfgFile string
13+
cfg *config.Config
14+
)
15+
16+
var rootCmd = &cobra.Command{
17+
Use: "docsgraph",
18+
Short: "DocsGraphContext — Pure Go GraphRAG MCP server",
19+
Long: `DocsGraphContext ingests unstructured documents, builds a knowledge graph
20+
with community detection, and exposes an MCP server + embedded Web UI.`,
21+
SilenceUsage: true,
22+
SilenceErrors: true,
23+
}
24+
25+
func Execute() {
26+
if err := rootCmd.Execute(); err != nil {
27+
fmt.Fprintln(os.Stderr, "error:", err)
28+
os.Exit(1)
29+
}
30+
}
31+
32+
func init() {
33+
cobra.OnInitialize(initConfig)
34+
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default ~/.docsgraph/config.yaml)")
35+
}
36+
37+
func initConfig() {
38+
var err error
39+
cfg, err = config.Load(cfgFile)
40+
if err != nil {
41+
fmt.Fprintln(os.Stderr, "config error:", err)
42+
os.Exit(1)
43+
}
44+
if err := os.MkdirAll(cfg.DataDir, 0755); err != nil {
45+
fmt.Fprintln(os.Stderr, "mkdir error:", err)
46+
os.Exit(1)
47+
}
48+
}

cmd/serve.go

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package cmd
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"net/http"
8+
"os"
9+
"os/signal"
10+
"syscall"
11+
"time"
12+
13+
"github.com/amit/docsgraphcontext/internal/api"
14+
"github.com/amit/docsgraphcontext/internal/embedder"
15+
"github.com/amit/docsgraphcontext/internal/llm"
16+
"github.com/amit/docsgraphcontext/internal/store"
17+
"github.com/spf13/cobra"
18+
)
19+
20+
var (
21+
serveHost string
22+
servePort int
23+
)
24+
25+
var serveCmd = &cobra.Command{
26+
Use: "serve",
27+
Short: "Start MCP + Web UI server",
28+
RunE: func(cmd *cobra.Command, args []string) error {
29+
// Override config with CLI flags
30+
if serveHost != "" {
31+
cfg.Server.Host = serveHost
32+
}
33+
if servePort != 0 {
34+
cfg.Server.Port = servePort
35+
}
36+
37+
st, err := store.Open(cfg.DBPath())
38+
if err != nil {
39+
return fmt.Errorf("open store: %w", err)
40+
}
41+
defer st.Close()
42+
43+
prov, err := llm.NewProvider(&cfg.LLM)
44+
if err != nil {
45+
return fmt.Errorf("llm provider: %w", err)
46+
}
47+
48+
emb := embedder.New(prov, cfg.Indexing.BatchSize)
49+
router := api.NewRouter(st, prov, emb, cfg)
50+
51+
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
52+
ln, err := net.Listen("tcp", addr)
53+
if err != nil {
54+
return fmt.Errorf("listen: %w", err)
55+
}
56+
57+
srv := &http.Server{Handler: router, ReadTimeout: 60 * time.Second, WriteTimeout: 120 * time.Second}
58+
59+
fmt.Fprintf(os.Stderr, "DocsGraphContext server running on http://%s\n", addr)
60+
fmt.Fprintf(os.Stderr, " Web UI: http://%s/\n", addr)
61+
fmt.Fprintf(os.Stderr, " MCP: http://%s/mcp\n", addr)
62+
fmt.Fprintf(os.Stderr, " API: http://%s/api/\n", addr)
63+
fmt.Fprintf(os.Stderr, " LLM: %s (%s)\n", prov.Name(), prov.ModelID())
64+
65+
// Graceful shutdown
66+
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
67+
defer stop()
68+
69+
go func() {
70+
if err := srv.Serve(ln); err != nil && err != http.ErrServerClosed {
71+
fmt.Fprintln(os.Stderr, "server error:", err)
72+
}
73+
}()
74+
75+
<-ctx.Done()
76+
fmt.Fprintln(os.Stderr, "\nShutting down...")
77+
shutCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
78+
defer cancel()
79+
return srv.Shutdown(shutCtx)
80+
},
81+
}
82+
83+
func init() {
84+
rootCmd.AddCommand(serveCmd)
85+
serveCmd.Flags().StringVar(&serveHost, "host", "", "Server host (overrides config)")
86+
serveCmd.Flags().IntVar(&servePort, "port", 0, "Server port (overrides config)")
87+
}

0 commit comments

Comments
 (0)