diff --git a/core/.dockerignore b/core/.dockerignore new file mode 100644 index 0000000..88b8f9e --- /dev/null +++ b/core/.dockerignore @@ -0,0 +1,53 @@ +# Gradle +.gradle/ +build/ +!gradle/wrapper/gradle-wrapper.jar +gradle-app.setting +!gradle-app.setting + +# IDE +.idea/ +.vscode/ +*.iml +*.ipr +*.iws + +# OS +.DS_Store +Thumbs.db + +# Logs +logs/ +*.log + +# Runtime +target/ +*.jar +!gradle/wrapper/gradle-wrapper.jar + +# Test results +.allure/ +build/reports/ +build/test-results/ + +# Environment +.env +.env.local +.env.*.local + +# Git +.git/ +.gitignore + +# Docker +Dockerfile +.dockerignore + +# Documentation +README.md +docs/ + +# Temporary files +*.tmp +*.swp +*.bak diff --git a/core/Dockerfile b/core/Dockerfile new file mode 100644 index 0000000..0b65ac7 --- /dev/null +++ b/core/Dockerfile @@ -0,0 +1,57 @@ +# Multi-stage build for Spring Boot application +FROM openjdk:21-jdk-slim as builder + +# Install curl, wget and unzip +RUN apt-get update && apt-get install -y curl wget unzip && \ + wget -O /tmp/gradle.zip https://services.gradle.org/distributions/gradle-8.5-bin.zip && \ + unzip /tmp/gradle.zip -d /opt && \ + ln -s /opt/gradle-8.5/bin/gradle /usr/local/bin/gradle && \ + rm /tmp/gradle.zip && \ + rm -rf /var/lib/apt/lists/* + +# Set working directory +WORKDIR /app + +# Copy build files +COPY build.gradle . +COPY settings.gradle . + +# Copy source code +COPY src/ src/ + +# Build the application +RUN gradle clean build -x test + +# Runtime stage +FROM openjdk:21-slim + +# Install curl for health checks +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* + +# Create app user +RUN groupadd -r appuser && useradd -r -g appuser appuser + +# Set working directory +WORKDIR /app + +# Copy the built jar from builder stage +COPY --from=builder /app/build/libs/*.jar app.jar + +# Create logs directory +RUN mkdir -p /app/logs && chown -R appuser:appuser /app + +# Switch to app user +USER appuser + +# Expose port +EXPOSE 8080 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ + CMD curl -f http://localhost:8080/actuator/health || exit 1 + +# JVM arguments for production +ENV JAVA_OPTS="-Xms512m -Xmx1g -XX:+UseG1GC -XX:+UseContainerSupport" + +# Run the application +ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"] diff --git a/core/build.gradle b/core/build.gradle new file mode 100644 index 0000000..0eae4d0 --- /dev/null +++ b/core/build.gradle @@ -0,0 +1,71 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.2.0' + id 'io.spring.dependency-management' version '1.1.4' +} + +group = 'com.opencontext' +version = '1.0.0' + +java { + sourceCompatibility = '21' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +dependencies { + // Spring Boot Starters + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-data-elasticsearch' + + // Database + runtimeOnly 'org.postgresql:postgresql' + + // API Documentation + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + + // Object Storage (MinIO) + implementation 'io.minio:minio:8.5.7' + + // HTTP Client + implementation 'org.springframework.boot:spring-boot-starter-webflux' + + // Utilities + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + implementation 'org.mapstruct:mapstruct:1.5.5.Final' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' + + // JSON Processing + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + + // Test Dependencies + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' + testImplementation 'org.testcontainers:junit-jupiter' + testImplementation 'org.testcontainers:postgresql' + testImplementation 'org.testcontainers:elasticsearch' +} + +dependencyManagement { + imports { + mavenBom "org.testcontainers:testcontainers-bom:1.19.3" + } +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/core/gradle/wrapper/gradle-wrapper.properties b/core/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..1af9e09 --- /dev/null +++ b/core/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/core/gradlew b/core/gradlew new file mode 100644 index 0000000..31e9ed3 --- /dev/null +++ b/core/gradlew @@ -0,0 +1,161 @@ +#!/bin/sh + +# +# Copyright Š 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for UNIX +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +exec "$JAVACMD" "$@" diff --git a/core/settings.gradle b/core/settings.gradle new file mode 100644 index 0000000..4d52ac5 --- /dev/null +++ b/core/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'core' diff --git a/core/src/main/java/com/opencontext/OpenContextApplication.java b/core/src/main/java/com/opencontext/OpenContextApplication.java new file mode 100644 index 0000000..5f84c4d --- /dev/null +++ b/core/src/main/java/com/opencontext/OpenContextApplication.java @@ -0,0 +1,16 @@ +package com.opencontext; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableJpaAuditing +@EnableAsync +public class OpenContextApplication { + + public static void main(String[] args) { + SpringApplication.run(OpenContextApplication.class, args); + } +} diff --git a/core/src/main/resources/application-docker.yml b/core/src/main/resources/application-docker.yml new file mode 100644 index 0000000..84b487e --- /dev/null +++ b/core/src/main/resources/application-docker.yml @@ -0,0 +1,78 @@ +spring: + application: + name: open-context-core + + # Database Configuration (Docker) + datasource: + url: jdbc:postgresql://postgres:5432/opencontext + username: user + password: password + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 10 + minimum-idle: 2 + + # JPA Configuration + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + + # Servlet Configuration + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB + + # Elasticsearch Configuration (Docker) + elasticsearch: + uris: http://elasticsearch:9200 + +# Ollama Configuration (Docker) +ollama: + base-url: http://ollama:11434 + model: dengcao/Qwen3-Embedding-0.6B:F16 + +# MinIO Configuration (Docker) +minio: + endpoint: http://minio:9000 + access-key: minioadmin + secret-key: minioadmin123! + bucket-name: opencontext-documents + +# Unstructured.io Configuration (Docker) +unstructured: + base-url: http://unstructured-api:8000 + +# Application Configuration +opencontext: + api: + key: dev-api-key-123 + processing: + chunk-size: 1000 + chunk-overlap: 200 + +# Management Configuration +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: when-authorized + +# Logging Configuration +logging: + level: + com.opencontext: DEBUG + org.springframework.data.elasticsearch: INFO + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + +server: + port: 8080 diff --git a/core/src/main/resources/application.yml b/core/src/main/resources/application.yml new file mode 100644 index 0000000..e4629b7 --- /dev/null +++ b/core/src/main/resources/application.yml @@ -0,0 +1,81 @@ +spring: + application: + name: open-context-core + + profiles: + active: dev + + # Database Configuration (Development) + datasource: + url: jdbc:postgresql://localhost:5432/opencontext + username: user + password: password + driver-class-name: org.postgresql.Driver + hikari: + maximum-pool-size: 10 + minimum-idle: 2 + + # JPA Configuration + jpa: + hibernate: + ddl-auto: create-drop + show-sql: true + properties: + hibernate: + dialect: org.hibernate.dialect.PostgreSQLDialect + format_sql: true + + # Servlet Configuration + servlet: + multipart: + max-file-size: 100MB + max-request-size: 100MB + +# Elasticsearch Configuration (Development) +elasticsearch: + hosts: localhost:9200 + +# Ollama Configuration (Development) +ollama: + base-url: http://localhost:11434 + model: dengcao/Qwen3-Embedding-0.6B:F16 + +# MinIO Configuration (Development) +minio: + endpoint: http://localhost:9000 + access-key: minioadmin + secret-key: minioadmin123! + bucket-name: opencontext-documents + +# Unstructured.io Configuration (Development) +unstructured: + base-url: http://localhost:8000 + +# Application Configuration +opencontext: + api: + key: dev-api-key-123 + processing: + chunk-size: 1000 + chunk-overlap: 200 + +# Management Configuration +management: + endpoints: + web: + exposure: + include: health,info,metrics + endpoint: + health: + show-details: when-authorized + +# Logging Configuration +logging: + level: + com.opencontext: DEBUG + org.springframework.data.elasticsearch: INFO + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + +server: + port: 8080 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..bcbed2c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,177 @@ +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: postgres + environment: + POSTGRES_DB: opencontext + POSTGRES_USER: user + POSTGRES_PASSWORD: password + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init/init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - opencontext-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U user"] + interval: 30s + timeout: 10s + retries: 5 + restart: unless-stopped + + # Elasticsearch with Nori plugin + elasticsearch: + build: + context: ./elasticsearch + dockerfile: Dockerfile + container_name: elasticsearch + ports: + - "9200:9200" + - "9300:9300" + environment: + - discovery.type=single-node + - xpack.security.enabled=false + - "ES_JAVA_OPTS=-Xms2g -Xmx2g" + volumes: + - es_data:/usr/share/elasticsearch/data + networks: + - opencontext-network + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9200/_cluster/health || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 60s + restart: unless-stopped + + # Ollama Model Downloader + ollama-init: + image: ollama/ollama:latest + container_name: opencontext-ollama-init + volumes: + - ollama_data:/root/.ollama + networks: + - opencontext-network + environment: + - OLLAMA_HOST=0.0.0.0 + entrypoint: /bin/sh + command: -c "ollama serve & sleep 10 && ollama pull dengcao/Qwen3-Embedding-0.6B:F16 && echo 'Model download completed' && pkill ollama" + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + + # Ollama Server for embeddings + ollama: + image: ollama/ollama:latest + container_name: ollama + ports: + - "11434:11434" + volumes: + - ollama_data:/root/.ollama + networks: + - opencontext-network + environment: + - OLLAMA_HOST=0.0.0.0 + # - OLLAMA_GPU_OVERHEAD=0 + # - OLLAMA_NUM_PARALLEL=1 + depends_on: + - ollama-init + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: 1 + capabilities: [gpu] + restart: unless-stopped + + # MinIO Object Storage + minio: + image: minio/minio:latest + container_name: minio + ports: + - "9000:9000" + - "9001:9001" + environment: + MINIO_ROOT_USER: minioadmin + MINIO_ROOT_PASSWORD: minioadmin123! + command: server /data --console-address ":9001" + volumes: + - minio_data:/data + networks: + - opencontext-network + restart: unless-stopped + + # Unstructured.io API for document parsing + unstructured-api: + image: quay.io/unstructured-io/unstructured-api:latest + container_name: unstructured-api + ports: + - "8000:8000" + networks: + - opencontext-network + restart: unless-stopped + + # Spring Boot Core Service + open-context-core: + build: + context: ./core + dockerfile: Dockerfile + image: opencontext/core:0.1.0 + container_name: open-context-core + ports: + - "8080:8080" + environment: + - SPRING_PROFILES_ACTIVE=docker + - SPRING_DATASOURCE_URL=jdbc:postgresql://postgres:5432/opencontext + - SPRING_DATASOURCE_USERNAME=user + - SPRING_DATASOURCE_PASSWORD=password + - ELASTICSEARCH_URL=http://elasticsearch:9200 + - OLLAMA_BASE_URL=http://ollama:11434 + - UNSTRUCTURED_API_URL=http://unstructured-api:8000 + - MINIO_URL=http://minio:9000 + - MINIO_ACCESS_KEY=${MINIO_ROOT_USER:-your-minio-access-key} + - MINIO_SECRET_KEY=${MINIO_ROOT_PASSWORD:-your-minio-secret-key} + depends_on: + postgres: + condition: service_healthy + elasticsearch: + condition: service_healthy + ollama: + condition: service_started + unstructured-api: + condition: service_started + minio: + condition: service_started + networks: + - opencontext-network + restart: unless-stopped + + # Node.js MCP Adapter + open-context-mcp-adapter: + build: + context: ./mcp-adapter + dockerfile: Dockerfile + image: opencontext/mcp-adapter:0.1.0 + container_name: open-context-mcp-adapter + depends_on: + - open-context-core + networks: + - opencontext-network + restart: unless-stopped + +volumes: + postgres_data: + es_data: + ollama_data: + minio_data: + +networks: + opencontext-network: + driver: bridge diff --git a/elasticsearch/Dockerfile b/elasticsearch/Dockerfile new file mode 100644 index 0000000..81e0a41 --- /dev/null +++ b/elasticsearch/Dockerfile @@ -0,0 +1,5 @@ +# Based on official Elasticsearch image +FROM docker.elastic.co/elasticsearch/elasticsearch:8.11.3 + +# Install nori (Korean morphological analyzer) plugin +RUN bin/elasticsearch-plugin install analysis-nori diff --git a/init/init.sql b/init/init.sql new file mode 100644 index 0000000..7b2032a --- /dev/null +++ b/init/init.sql @@ -0,0 +1,64 @@ +-- OpenContext Database Initialization Script +-- This script creates the PostgreSQL tables according to PRD specifications + +-- Create extension for UUID generation +CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; + +-- Source Documents Table +CREATE TABLE source_documents ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + original_filename VARCHAR(255) NOT NULL, + file_storage_path VARCHAR(1024) NOT NULL, + file_type VARCHAR(50) NOT NULL, + file_size BIGINT NOT NULL, + file_checksum VARCHAR(64) NOT NULL, + ingestion_status VARCHAR(20) NOT NULL DEFAULT 'PENDING' CHECK (ingestion_status IN ('PENDING', 'PARSING', 'CHUNKING', 'EMBEDDING', 'INDEXING', 'COMPLETED', 'ERROR', 'DELETING')), + error_message TEXT, + last_ingested_at TIMESTAMPTZ, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT uq_file_checksum UNIQUE (file_checksum) +); + +-- Document Chunks Table +CREATE TABLE document_chunks ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + source_document_id UUID NOT NULL REFERENCES source_documents(id) ON DELETE CASCADE, + parent_chunk_id UUID REFERENCES document_chunks(id) ON DELETE CASCADE, + sequence_in_document INTEGER NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Create indexes for better performance +CREATE INDEX idx_chunk_source_document_id ON document_chunks(source_document_id); +CREATE INDEX idx_chunk_parent_chunk_id ON document_chunks(parent_chunk_id); + +-- Add comments for better documentation +COMMENT ON TABLE source_documents IS 'Table storing management information for all uploaded source files'; +COMMENT ON COLUMN source_documents.id IS 'Primary key - unique identifier for each original document'; +COMMENT ON COLUMN source_documents.original_filename IS 'Original filename uploaded by user'; +COMMENT ON COLUMN source_documents.file_storage_path IS 'Actual storage path in Object Storage (MinIO)'; +COMMENT ON COLUMN source_documents.file_type IS 'File type (PDF, MARKDOWN, etc.)'; +COMMENT ON COLUMN source_documents.file_size IS 'File size in bytes'; +COMMENT ON COLUMN source_documents.file_checksum IS 'SHA-256 hash (for duplicate prevention)'; +COMMENT ON COLUMN source_documents.ingestion_status IS 'Current status of document processing pipeline (PENDING, PARSING, CHUNKING, EMBEDDING, INDEXING, COMPLETED, ERROR, DELETING)'; +COMMENT ON COLUMN source_documents.error_message IS 'Detailed error message when status is ERROR'; +COMMENT ON COLUMN source_documents.last_ingested_at IS 'Last successful ingestion completion time'; +COMMENT ON COLUMN source_documents.created_at IS 'Time when first created'; +COMMENT ON COLUMN source_documents.updated_at IS 'Time when last updated'; +COMMENT ON CONSTRAINT uq_file_checksum ON source_documents IS 'Constraint that enforces uniqueness of file_checksum values in the table'; + +COMMENT ON TABLE document_chunks IS 'Table storing metadata and hierarchical structure information for each Structured Chunk'; +COMMENT ON COLUMN document_chunks.id IS 'Primary key - unique identifier for each Structured Chunk (same as Elasticsearch chunkId)'; +COMMENT ON COLUMN document_chunks.source_document_id IS 'Original document ID'; +COMMENT ON COLUMN document_chunks.parent_chunk_id IS 'Parent chunk ID - hierarchical structure representation'; +COMMENT ON COLUMN document_chunks.sequence_in_document IS 'Order among sibling chunks with the same parent_chunk_id in the original document'; + +-- Insert sample data for development (optional) +-- This can be removed in production +INSERT INTO source_documents (original_filename, file_storage_path, file_type, file_size, file_checksum, ingestion_status) +VALUES + ('sample-guide.pdf', 'documents/sample-guide.pdf', 'PDF', 1024000, 'a1b2c3d4e5f6...', 'COMPLETED'), + ('spring-security-reference.pdf', 'documents/spring-security-reference.pdf', 'PDF', 2048000, 'f6e5d4c3b2a1...', 'PENDING'); + +COMMIT; \ No newline at end of file diff --git a/mcp-adapter/.dockerignore b/mcp-adapter/.dockerignore new file mode 100644 index 0000000..a98fdec --- /dev/null +++ b/mcp-adapter/.dockerignore @@ -0,0 +1,37 @@ +# Dependencies +node_modules +npm-debug.log* + +# Environment files +.env +.env.local +.env.production + +# Logs +logs +*.log + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Coverage directory used by tools like istanbul +coverage + +# IDE +.vscode +.idea + +# OS +.DS_Store +Thumbs.db + +# Git +.git +.gitignore + +# Docker +Dockerfile +.dockerignore diff --git a/mcp-adapter/Dockerfile b/mcp-adapter/Dockerfile new file mode 100644 index 0000000..f6cf8db --- /dev/null +++ b/mcp-adapter/Dockerfile @@ -0,0 +1,20 @@ +# MCP Adapter Dockerfile +FROM node:18-alpine + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install dependencies (only devDependencies for nodemon) +RUN npm install + +# Copy source code +COPY . . + +# Expose port +EXPOSE 3000 + +# Start the application +CMD ["node", "index.js"] diff --git a/mcp-adapter/index.js b/mcp-adapter/index.js new file mode 100644 index 0000000..5f316c1 --- /dev/null +++ b/mcp-adapter/index.js @@ -0,0 +1,71 @@ +const http = require('http'); + +// Simple MCP Adapter - Docker Health Check Only +const server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Access-Control-Allow-Origin', '*'); + res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS'); + res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); + + if (req.method === 'OPTIONS') { + res.writeHead(200); + res.end(); + return; + } + + if (req.method === 'GET' && req.url === '/health') { + const health = { + status: 'healthy', + service: 'mcp-adapter', + timestamp: new Date().toISOString(), + uptime: process.uptime(), + version: '1.0.0' + }; + + res.writeHead(200); + res.end(JSON.stringify(health, null, 2)); + } else if (req.method === 'GET' && req.url === '/') { + const info = { + service: 'OpenContext MCP Adapter', + version: '1.0.0', + status: 'running', + endpoints: { + health: '/health', + info: '/' + } + }; + + res.writeHead(200); + res.end(JSON.stringify(info, null, 2)); + } else { + res.writeHead(404); + res.end(JSON.stringify({ + error: 'Endpoint not found', + available: ['/', '/health'] + }, null, 2)); + } +}); + +const PORT = process.env.PORT || 3000; +server.listen(PORT, '0.0.0.0', () => { + console.log(`🚀 MCP Adapter running on port ${PORT}`); + console.log(`📊 Health check: http://localhost:${PORT}/health`); + console.log(`â„šī¸ Info: http://localhost:${PORT}/`); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + console.log('SIGTERM received, shutting down gracefully'); + server.close(() => { + console.log('Server closed'); + process.exit(0); + }); +}); + +process.on('SIGINT', () => { + console.log('SIGINT received, shutting down gracefully'); + server.close(() => { + console.log('Server closed'); + process.exit(0); + }); +}); diff --git a/mcp-adapter/package.json b/mcp-adapter/package.json new file mode 100644 index 0000000..0e0cb31 --- /dev/null +++ b/mcp-adapter/package.json @@ -0,0 +1,20 @@ +{ + "name": "mcp-adapter", + "version": "1.0.0", + "description": "OpenContext MCP Adapter", + "main": "index.js", + "scripts": { + "start": "node index.js", + "dev": "nodemon index.js" + }, + "keywords": ["mcp", "adapter"], + "author": "OpenContext Team", + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "nodemon": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } +}