Add bearer authentication based on namespace
While there can be namespaces without a secret, commonly the namespace should basically be the username and the password the login secret.
This commit is contained in:
parent
a8694850b1
commit
f2f7879f42
|
|
@ -0,0 +1,8 @@
|
||||||
|
-- Auth secrets for bearer token authentication
|
||||||
|
CREATE TABLE IF NOT EXISTS auth_secrets (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
namespace_id UUID NOT NULL REFERENCES namespaces(id) ON DELETE CASCADE,
|
||||||
|
secret_hash TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE(namespace_id)
|
||||||
|
);
|
||||||
|
|
@ -8,12 +8,12 @@
|
||||||
## Tools
|
## Tools
|
||||||
### `save_context`
|
### `save_context`
|
||||||
Persist a context blob with metadata.
|
Persist a context blob with metadata.
|
||||||
- input: `{ namespace: string, key?: string, content: any, tags?: string[], ttl_seconds?: number }`
|
- input: `{ key?: string, content: any, tags?: string[], ttl_seconds?: number }`
|
||||||
- output: `{ id: string, created_at: string }`
|
- output: `{ id: string, created_at: string }`
|
||||||
|
|
||||||
### `recall_context`
|
### `recall_context`
|
||||||
Fetch context by key/tags/time range.
|
Fetch context by key/tags/time range.
|
||||||
- input: `{ namespace: string, key?: string, tags?: string[], limit?: number, since?: string }`
|
- input: `{ key?: string, tags?: string[], limit?: number, since?: string }`
|
||||||
- output: `{ items: Array<{id:string, key?:string, content:any, tags?:string[], created_at:string}> }`
|
- output: `{ items: Array<{id:string, key?:string, content:any, tags?:string[], created_at:string}> }`
|
||||||
|
|
||||||
### `embed_text`
|
### `embed_text`
|
||||||
|
|
@ -23,22 +23,22 @@ Return vector embedding for given text(s).
|
||||||
|
|
||||||
### `upsert_memory`
|
### `upsert_memory`
|
||||||
Upsert text+metadata into vector store.
|
Upsert text+metadata into vector store.
|
||||||
- input: `{ namespace: string, items: Array<{id?:string, text:string, metadata?:object, embedding?:number[]}> }`
|
- input: `{ items: Array<{id?:string, text:string, metadata?:object, embedding?:number[]}> }`
|
||||||
- output: `{ upserted: number }`
|
- output: `{ upserted: number }`
|
||||||
|
|
||||||
### `search_memory`
|
### `search_memory`
|
||||||
Vector + keyword hybrid search.
|
Vector + keyword hybrid search.
|
||||||
- input: `{ namespace: string, query: { text?: string, embedding?: number[], k?: number, filter?: object } }`
|
- input: `{ query: { text?: string, embedding?: number[], k?: number, filter?: object } }`
|
||||||
- output: `{ matches: Array<{id:string, score:number, text?:string, metadata?:object}> }`
|
- output: `{ matches: Array<{id:string, score:number, text?:string, metadata?:object}> }`
|
||||||
|
|
||||||
### `warm_cache`
|
### `warm_cache`
|
||||||
Precompute embeddings for recent items.
|
Precompute embeddings for recent items.
|
||||||
- input: `{ namespace: string, since?: string }`
|
- input: `{ since?: string }`
|
||||||
- output: `{ queued: number }`
|
- output: `{ queued: number }`
|
||||||
|
|
||||||
### `sync_semantic`
|
### `sync_semantic`
|
||||||
Promote episodic rows into semantic (chunks + embeddings) storage.
|
Promote episodic rows into semantic (chunks + embeddings) storage.
|
||||||
- input: `{ namespace: string, max_batch?: number }`
|
- input: `{ max_batch?: number }`
|
||||||
- output: `{ processed: number, pending: number }`
|
- output: `{ processed: number, pending: number }`
|
||||||
- Notes: skips items with `sensitivity="secret"` or expired TTL; requires DAL job support.
|
- Notes: skips items with `sensitivity="secret"` or expired TTL; requires DAL job support.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ If JSON parsing is brittle, accept a single line command and parse it:
|
||||||
|
|
||||||
@tool <name> {json-args}
|
@tool <name> {json-args}
|
||||||
|
|
||||||
Example: @tool kom.memory.v1.search_memory {"namespace":"project:metal", "query":{"text":"embedding model"}}
|
Example: @tool kom.memory.v1.search_memory {"query":{"text":"embedding model"}}
|
||||||
|
|
||||||
## 3) Prompt Template (drop-in)
|
## 3) Prompt Template (drop-in)
|
||||||
Use this system message for happy-code/claude-code sessions:
|
Use this system message for happy-code/claude-code sessions:
|
||||||
|
|
@ -69,7 +69,7 @@ while True:
|
||||||
System: (template above + list of tools & schemas)
|
System: (template above + list of tools & schemas)
|
||||||
User: Save this note: "Embedding model comparison takeaways" into project:metal
|
User: Save this note: "Embedding model comparison takeaways" into project:metal
|
||||||
Assistant:
|
Assistant:
|
||||||
{"thought":"need to upsert note","action":{"tool":"kom.memory.v1.upsert_memory","args":{"namespace":"project:metal","items":[{"text":"Embedding model comparison takeaways"}]}}}
|
{"thought":"need to upsert note","action":{"tool":"kom.memory.v1.upsert_memory","args":{"items":[{"text":"Embedding model comparison takeaways"}]}}}
|
||||||
Tool (kom.memory.v1.upsert_memory): { "upserted": 1 }
|
Tool (kom.memory.v1.upsert_memory): { "upserted": 1 }
|
||||||
Assistant:
|
Assistant:
|
||||||
{"final":{"content":{"status":"ok","upserted":1}}}
|
{"final":{"content":{"status":"ok","upserted":1}}}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ The Kompanion MCP backend exposes `kom.memory.v1.save_context` and `kom.memory.v
|
||||||
{
|
{
|
||||||
"tool": "kom.memory.v1.save_context",
|
"tool": "kom.memory.v1.save_context",
|
||||||
"arguments": {
|
"arguments": {
|
||||||
"namespace": "project:metal",
|
|
||||||
"key": "codey/session",
|
"key": "codey/session",
|
||||||
"content": {"summary": "Refactored PgDal for TTL support"},
|
"content": {"summary": "Refactored PgDal for TTL support"},
|
||||||
"tags": ["codey", "memory"]
|
"tags": ["codey", "memory"]
|
||||||
|
|
@ -26,7 +25,6 @@ The Kompanion MCP backend exposes `kom.memory.v1.save_context` and `kom.memory.v
|
||||||
{
|
{
|
||||||
"tool": "kom.memory.v1.recall_context",
|
"tool": "kom.memory.v1.recall_context",
|
||||||
"arguments": {
|
"arguments": {
|
||||||
"namespace": "thread:123",
|
|
||||||
"limit": 5,
|
"limit": 5,
|
||||||
"tags": ["task"]
|
"tags": ["task"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -281,21 +281,10 @@ std::optional<NamespaceRow> PgDal::ensureNamespace(const std::string& name) {
|
||||||
if (!connected_) {
|
if (!connected_) {
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
if (!useInMemory_ && hasDatabase()) {
|
if (auto existing = findNamespace(name)) {
|
||||||
return sqlEnsureNamespace(name);
|
return existing;
|
||||||
}
|
}
|
||||||
|
return createNamespaceWithSecret(name).first;
|
||||||
auto it = namespacesByName_.find(name);
|
|
||||||
if (it != namespacesByName_.end()) {
|
|
||||||
return it->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
NamespaceRow row;
|
|
||||||
row.id = allocateId(nextNamespaceId_, "ns_");
|
|
||||||
row.name = name;
|
|
||||||
namespacesByName_[name] = row;
|
|
||||||
namespacesById_[row.id] = row;
|
|
||||||
return row;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<NamespaceRow> PgDal::findNamespace(const std::string& name) const {
|
std::optional<NamespaceRow> PgDal::findNamespace(const std::string& name) const {
|
||||||
|
|
@ -310,22 +299,6 @@ std::optional<NamespaceRow> PgDal::findNamespace(const std::string& name) const
|
||||||
return it->second;
|
return it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
NamespaceRow PgDal::sqlEnsureNamespace(const std::string& name) {
|
|
||||||
QSqlDatabase db = database();
|
|
||||||
QSqlQuery query(db);
|
|
||||||
query.prepare(QStringLiteral(
|
|
||||||
"INSERT INTO namespaces (name) VALUES (:name) "
|
|
||||||
"ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name "
|
|
||||||
"RETURNING id::text, name;"));
|
|
||||||
query.bindValue(QStringLiteral(":name"), QString::fromStdString(name));
|
|
||||||
if (!query.exec() || !query.next()) {
|
|
||||||
throw std::runtime_error(query.lastError().text().toStdString());
|
|
||||||
}
|
|
||||||
NamespaceRow row;
|
|
||||||
row.id = query.value(0).toString().toStdString();
|
|
||||||
row.name = query.value(1).toString().toStdString();
|
|
||||||
return row;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<NamespaceRow> PgDal::sqlFindNamespace(const std::string& name) const {
|
std::optional<NamespaceRow> PgDal::sqlFindNamespace(const std::string& name) const {
|
||||||
QSqlDatabase db = database();
|
QSqlDatabase db = database();
|
||||||
|
|
@ -345,6 +318,103 @@ std::optional<NamespaceRow> PgDal::sqlFindNamespace(const std::string& name) con
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::pair<NamespaceRow, std::string> PgDal::createNamespaceWithSecret(const std::string& name) {
|
||||||
|
if (!connected_) {
|
||||||
|
throw std::runtime_error("PgDal not connected");
|
||||||
|
}
|
||||||
|
if (!useInMemory_ && hasDatabase()) {
|
||||||
|
return sqlCreateNamespaceWithSecret(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-memory implementation
|
||||||
|
auto it = namespacesByName_.find(name);
|
||||||
|
if (it != namespacesByName_.end()) {
|
||||||
|
// For in-memory, we don't have secrets, so we can't return one.
|
||||||
|
// This path should ideally not be taken in production.
|
||||||
|
return {it->second, ""};
|
||||||
|
}
|
||||||
|
|
||||||
|
NamespaceRow row;
|
||||||
|
row.id = allocateId(nextNamespaceId_, "ns_");
|
||||||
|
row.name = name;
|
||||||
|
namespacesByName_[name] = row;
|
||||||
|
namespacesById_[row.id] = row;
|
||||||
|
|
||||||
|
// Secrets are not supported in-memory for now
|
||||||
|
return {row, ""};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AuthSecret> PgDal::findSecretByNamespaceId(const std::string& namespaceId) const {
|
||||||
|
if (!useInMemory_ && hasDatabase()) {
|
||||||
|
return sqlFindSecretByNamespaceId(namespaceId);
|
||||||
|
}
|
||||||
|
// In-memory implementation does not support secrets
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<NamespaceRow, std::string> PgDal::sqlCreateNamespaceWithSecret(const std::string& name) {
|
||||||
|
QSqlDatabase db = database();
|
||||||
|
QSqlQuery query(db);
|
||||||
|
|
||||||
|
// 1. Create the namespace
|
||||||
|
query.prepare(QStringLiteral(
|
||||||
|
"INSERT INTO namespaces (name) VALUES (:name) "
|
||||||
|
"ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name "
|
||||||
|
"RETURNING id::text, name;"));
|
||||||
|
query.bindValue(QStringLiteral(":name"), QString::fromStdString(name));
|
||||||
|
if (!query.exec() || !query.next()) {
|
||||||
|
throw std::runtime_error(query.lastError().text().toStdString());
|
||||||
|
}
|
||||||
|
NamespaceRow row;
|
||||||
|
row.id = query.value(0).toString().toStdString();
|
||||||
|
row.name = query.value(1).toString().toStdString();
|
||||||
|
|
||||||
|
// 2. Generate and store the secret
|
||||||
|
QByteArray secretData(32, 0);
|
||||||
|
for (int i = 0; i < secretData.size(); ++i) {
|
||||||
|
secretData[i] = static_cast<char>(QRandomGenerator::system()->generate() % 256);
|
||||||
|
}
|
||||||
|
const std::string secret = secretData.toHex().toStdString();
|
||||||
|
const QByteArray secretHash = QCryptographicHash::hash(QByteArray::fromStdString(secret), QCryptographicHash::Sha256);
|
||||||
|
const std::string secretHashStr = secretHash.toHex().toStdString();
|
||||||
|
|
||||||
|
sqlInsertSecret(row.id, secretHashStr);
|
||||||
|
|
||||||
|
return {row, secret};
|
||||||
|
}
|
||||||
|
|
||||||
|
void PgDal::sqlInsertSecret(const std::string& namespaceId, const std::string& secretHash) {
|
||||||
|
QSqlDatabase db = database();
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(QStringLiteral(
|
||||||
|
"INSERT INTO auth_secrets (namespace_id, secret_hash) "
|
||||||
|
"VALUES (:namespace_id::uuid, :secret_hash) "
|
||||||
|
"ON CONFLICT (namespace_id) DO UPDATE SET secret_hash = EXCLUDED.secret_hash;"));
|
||||||
|
query.bindValue(QStringLiteral(":namespace_id"), QString::fromStdString(namespaceId));
|
||||||
|
query.bindValue(QStringLiteral(":secret_hash"), QString::fromStdString(secretHash));
|
||||||
|
if (!query.exec()) {
|
||||||
|
throw std::runtime_error(query.lastError().text().toStdString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<AuthSecret> PgDal::sqlFindSecretByNamespaceId(const std::string& namespaceId) const {
|
||||||
|
QSqlDatabase db = database();
|
||||||
|
QSqlQuery query(db);
|
||||||
|
query.prepare(QStringLiteral(
|
||||||
|
"SELECT secret_hash FROM auth_secrets WHERE namespace_id = :namespace_id::uuid"));
|
||||||
|
query.bindValue(QStringLiteral(":namespace_id"), QString::fromStdString(namespaceId));
|
||||||
|
if (!query.exec()) {
|
||||||
|
throw std::runtime_error(query.lastError().text().toStdString());
|
||||||
|
}
|
||||||
|
if (!query.next()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
AuthSecret secret;
|
||||||
|
secret.secret_hash = query.value(0).toString().toStdString();
|
||||||
|
return secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
std::string PgDal::upsertItem(const ItemRow& row) {
|
std::string PgDal::upsertItem(const ItemRow& row) {
|
||||||
if (!connected_) {
|
if (!connected_) {
|
||||||
throw std::runtime_error("PgDal not connected");
|
throw std::runtime_error("PgDal not connected");
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@
|
||||||
|
|
||||||
#include <QSqlDatabase>
|
#include <QSqlDatabase>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QRandomGenerator>
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
@ -47,6 +49,10 @@ struct EmbeddingRow {
|
||||||
std::vector<float> vector;
|
std::vector<float> vector;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct AuthSecret {
|
||||||
|
std::string secret_hash;
|
||||||
|
};
|
||||||
|
|
||||||
class PgDal final : public IDatabase {
|
class PgDal final : public IDatabase {
|
||||||
public:
|
public:
|
||||||
PgDal();
|
PgDal();
|
||||||
|
|
@ -57,8 +63,10 @@ public:
|
||||||
void commit();
|
void commit();
|
||||||
void rollback();
|
void rollback();
|
||||||
|
|
||||||
|
std::pair<NamespaceRow, std::string> createNamespaceWithSecret(const std::string& name);
|
||||||
std::optional<NamespaceRow> ensureNamespace(const std::string& name);
|
std::optional<NamespaceRow> ensureNamespace(const std::string& name);
|
||||||
std::optional<NamespaceRow> findNamespace(const std::string& name) const;
|
std::optional<NamespaceRow> findNamespace(const std::string& name) const;
|
||||||
|
std::optional<AuthSecret> findSecretByNamespaceId(const std::string& namespaceId) const;
|
||||||
|
|
||||||
std::string upsertItem(const ItemRow& row);
|
std::string upsertItem(const ItemRow& row);
|
||||||
std::vector<std::string> upsertChunks(const std::vector<ChunkRow>& chunks);
|
std::vector<std::string> upsertChunks(const std::vector<ChunkRow>& chunks);
|
||||||
|
|
@ -113,8 +121,11 @@ private:
|
||||||
QSqlDatabase database() const;
|
QSqlDatabase database() const;
|
||||||
ConnectionConfig parseDsn(const std::string& dsn) const;
|
ConnectionConfig parseDsn(const std::string& dsn) const;
|
||||||
|
|
||||||
NamespaceRow sqlEnsureNamespace(const std::string& name);
|
std::pair<NamespaceRow, std::string> sqlCreateNamespaceWithSecret(const std::string& name);
|
||||||
|
std::optional<NamespaceRow> sqlEnsureNamespace(const std::string& name);
|
||||||
std::optional<NamespaceRow> sqlFindNamespace(const std::string& name) const;
|
std::optional<NamespaceRow> sqlFindNamespace(const std::string& name) const;
|
||||||
|
std::optional<AuthSecret> sqlFindSecretByNamespaceId(const std::string& namespaceId) const;
|
||||||
|
void sqlInsertSecret(const std::string& namespaceId, const std::string& secretHash);
|
||||||
std::pair<std::string, int> sqlUpsertItem(const ItemRow& row);
|
std::pair<std::string, int> sqlUpsertItem(const ItemRow& row);
|
||||||
std::vector<std::string> sqlUpsertChunks(const std::vector<ChunkRow>& chunks);
|
std::vector<std::string> sqlUpsertChunks(const std::vector<ChunkRow>& chunks);
|
||||||
void sqlUpsertEmbeddings(const std::vector<EmbeddingRow>& embeddings);
|
void sqlUpsertEmbeddings(const std::vector<EmbeddingRow>& embeddings);
|
||||||
|
|
|
||||||
10
src/main.cpp
10
src/main.cpp
|
|
@ -26,6 +26,7 @@
|
||||||
#include "mcp/KomMcpServer.hpp"
|
#include "mcp/KomMcpServer.hpp"
|
||||||
#include "mcp/KompanionQtServer.hpp"
|
#include "mcp/KompanionQtServer.hpp"
|
||||||
#include "mcp/RegisterTools.hpp"
|
#include "mcp/RegisterTools.hpp"
|
||||||
|
#include "dal/PgDal.hpp"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
|
@ -199,6 +200,13 @@ int main(int argc, char **argv)
|
||||||
std::cerr << "[kom_mcp] PG_DSN not set; DAL will fall back to stubbed mode. Configure Database/PgDsn to enable persistence.\n";
|
std::cerr << "[kom_mcp] PG_DSN not set; DAL will fall back to stubbed mode. Configure Database/PgDsn to enable persistence.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
kom::PgDal dal;
|
||||||
|
if (effectiveDsn) {
|
||||||
|
if (!dal.connect(*effectiveDsn)) {
|
||||||
|
std::cerr << "[kom_mcp] Failed to connect to database; DAL will fall back to stubbed mode.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parser.isSet(listOption)) {
|
if (parser.isSet(listOption)) {
|
||||||
for (const auto &toolName : logic.listTools()) {
|
for (const auto &toolName : logic.listTools()) {
|
||||||
std::cout << toolName << "\n";
|
std::cout << toolName << "\n";
|
||||||
|
|
@ -237,7 +245,7 @@ int main(int argc, char **argv)
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
KompanionQtServer server(backend, &logic);
|
KompanionQtServer server(backend, &logic, &dal);
|
||||||
if (backend == QStringLiteral("stdio")) {
|
if (backend == QStringLiteral("stdio")) {
|
||||||
server.start();
|
server.start();
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -634,4 +634,22 @@ inline std::string recall_context(const std::string& reqJson) {
|
||||||
return os.str();
|
return os.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::string embed_text(const std::string& reqJson) {
|
||||||
|
const std::string nsName = detail::extract_string_field(reqJson, "namespace");
|
||||||
|
if (nsName.empty()) {
|
||||||
|
return detail::error_response("bad_request", "namespace is required");
|
||||||
|
}
|
||||||
|
// For now, just return a dummy successful response
|
||||||
|
return "{\"model\":\"stub-model\",\"vectors\":[[0.1,0.2,0.3]]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::string warm_cache(const std::string& reqJson) {
|
||||||
|
const std::string nsName = detail::extract_string_field(reqJson, "namespace");
|
||||||
|
if (nsName.empty()) {
|
||||||
|
return detail::error_response("bad_request", "namespace is required");
|
||||||
|
}
|
||||||
|
// For now, just return a dummy successful response
|
||||||
|
return "{\"queued\":0}";
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace Handlers
|
} // namespace Handlers
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@
|
||||||
#include <QtCore/QLoggingCategory>
|
#include <QtCore/QLoggingCategory>
|
||||||
#include <QtCore/QStandardPaths>
|
#include <QtCore/QStandardPaths>
|
||||||
#include <QtCore/QStringList>
|
#include <QtCore/QStringList>
|
||||||
|
#include <QtCore/QJsonValue>
|
||||||
|
#include <QStringView>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
using namespace Qt::Literals::StringLiterals;
|
using namespace Qt::Literals::StringLiterals;
|
||||||
|
|
@ -34,9 +37,10 @@ QString normaliseToolName(const QString &defaultNamespace, const QString &rawNam
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
KompanionQtServer::KompanionQtServer(const QString &backend, KomMcpServer *logic, QObject *parent)
|
KompanionQtServer::KompanionQtServer(const QString &backend, KomMcpServer *logic, kom::PgDal* dal, QObject *parent)
|
||||||
: QMcpServer(backend, parent)
|
: QMcpServer(backend, parent)
|
||||||
, m_logic(logic)
|
, m_logic(logic)
|
||||||
|
, m_dal(dal)
|
||||||
{
|
{
|
||||||
setProtocolVersion(QtMcp::ProtocolVersion::Latest);
|
setProtocolVersion(QtMcp::ProtocolVersion::Latest);
|
||||||
setSupportedProtocolVersions({QtMcp::ProtocolVersion::v2024_11_05,
|
setSupportedProtocolVersions({QtMcp::ProtocolVersion::v2024_11_05,
|
||||||
|
|
@ -80,6 +84,51 @@ KompanionQtServer::KompanionQtServer(const QString &backend, KomMcpServer *logic
|
||||||
}
|
}
|
||||||
|
|
||||||
const QJsonObject args = request.params().arguments();
|
const QJsonObject args = request.params().arguments();
|
||||||
|
const QJsonValue authTokenValue = args.value("auth_token");
|
||||||
|
const QString authToken = authTokenValue.toString();
|
||||||
|
|
||||||
|
if (m_dal) {
|
||||||
|
const QStringList parts = authToken.split(':');
|
||||||
|
if (parts.size() != 2) {
|
||||||
|
if (error) {
|
||||||
|
error->setCode(401);
|
||||||
|
error->setMessage("Unauthorized: Invalid auth token format");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const std::string namespaceName = parts[0].toStdString();
|
||||||
|
const std::string secret = parts[1].toStdString();
|
||||||
|
|
||||||
|
auto ns = m_dal->findNamespace(namespaceName);
|
||||||
|
if (!ns) {
|
||||||
|
if (error) {
|
||||||
|
error->setCode(401);
|
||||||
|
error->setMessage("Unauthorized: Namespace not found");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto authSecret = m_dal->findSecretByNamespaceId(ns->id);
|
||||||
|
if (!authSecret) {
|
||||||
|
if (error) {
|
||||||
|
error->setCode(401);
|
||||||
|
error->setMessage("Unauthorized: No secret found for namespace");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray providedSecretHash = QCryptographicHash::hash(QByteArray::fromStdString(secret), QCryptographicHash::Sha256).toHex();
|
||||||
|
const QByteArray storedSecretHash = QByteArray::fromStdString(authSecret->secret_hash).toHex();
|
||||||
|
|
||||||
|
if (providedSecretHash != storedSecretHash) {
|
||||||
|
if (error) {
|
||||||
|
error->setCode(401);
|
||||||
|
error->setMessage("Unauthorized: Invalid secret");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const QByteArray payload = QJsonDocument(args).toJson(QJsonDocument::Compact);
|
const QByteArray payload = QJsonDocument(args).toJson(QJsonDocument::Compact);
|
||||||
const std::string responseStr = m_logic->dispatch(toolKey, payload.toStdString());
|
const std::string responseStr = m_logic->dispatch(toolKey, payload.toStdString());
|
||||||
const QByteArray jsonBytes = QByteArray::fromStdString(responseStr);
|
const QByteArray jsonBytes = QByteArray::fromStdString(responseStr);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
#include <QtMcpServer/QMcpServer>
|
#include <QtMcpServer/QMcpServer>
|
||||||
#include <QtMcpCommon/QMcpTool>
|
#include <QtMcpCommon/QMcpTool>
|
||||||
|
#include "../dal/PgDal.hpp"
|
||||||
|
|
||||||
#include "KomMcpServer.hpp"
|
#include "KomMcpServer.hpp"
|
||||||
|
|
||||||
|
|
@ -11,11 +12,12 @@ class KompanionQtServer : public QMcpServer
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
public:
|
public:
|
||||||
KompanionQtServer(const QString &backend, KomMcpServer *logic, QObject *parent = nullptr);
|
KompanionQtServer(const QString &backend, KomMcpServer *logic, kom::PgDal* dal, QObject *parent = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QList<QMcpTool> loadToolsFromSchema() const;
|
QList<QMcpTool> loadToolsFromSchema() const;
|
||||||
|
|
||||||
KomMcpServer *m_logic = nullptr;
|
KomMcpServer *m_logic = nullptr;
|
||||||
|
kom::PgDal* m_dal = nullptr;
|
||||||
QList<QMcpTool> m_tools;
|
QList<QMcpTool> m_tools;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@
|
||||||
inline void register_default_tools(KomMcpServer& server) {
|
inline void register_default_tools(KomMcpServer& server) {
|
||||||
server.registerTool("kom.memory.v1.save_context", Handlers::save_context);
|
server.registerTool("kom.memory.v1.save_context", Handlers::save_context);
|
||||||
server.registerTool("kom.memory.v1.recall_context", Handlers::recall_context);
|
server.registerTool("kom.memory.v1.recall_context", Handlers::recall_context);
|
||||||
|
server.registerTool("kom.memory.v1.embed_text", Handlers::embed_text);
|
||||||
server.registerTool("kom.memory.v1.upsert_memory", Handlers::upsert_memory);
|
server.registerTool("kom.memory.v1.upsert_memory", Handlers::upsert_memory);
|
||||||
server.registerTool("kom.memory.v1.search_memory", Handlers::search_memory);
|
server.registerTool("kom.memory.v1.search_memory", Handlers::search_memory);
|
||||||
|
server.registerTool("kom.memory.v1.warm_cache", Handlers::warm_cache);
|
||||||
server.registerTool("kom.local.v1.backup.export_encrypted", Handlers::backup_export_encrypted);
|
server.registerTool("kom.local.v1.backup.export_encrypted", Handlers::backup_export_encrypted);
|
||||||
server.registerTool("kom.local.v1.backup.import_encrypted", Handlers::backup_import_encrypted);
|
server.registerTool("kom.local.v1.backup.import_encrypted", Handlers::backup_import_encrypted);
|
||||||
server.registerTool("kom.meta.v1.project_snapshot", Handlers::project_snapshot);
|
server.registerTool("kom.meta.v1.project_snapshot", Handlers::project_snapshot);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
#pragma once
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
struct Scope {
|
|
||||||
std::string namespace_id;
|
|
||||||
std::string user_id;
|
|
||||||
bool valid() const { return !namespace_id.empty(); }
|
|
||||||
};
|
|
||||||
|
|
||||||
// In a real qtmcp server, these would be derived from session/auth.
|
|
||||||
inline Scope currentScope() {
|
|
||||||
// TODO: inject from session context / env. For now, return empty (invalid).
|
|
||||||
return Scope{};
|
|
||||||
}
|
|
||||||
|
|
@ -39,6 +39,17 @@ std::string recallResp = server.dispatch("kom.memory.v1.recall_context", recallR
|
||||||
if (!expect_contains(recallResp, "\"items\"", "recall_context items")) return 1;
|
if (!expect_contains(recallResp, "\"items\"", "recall_context items")) return 1;
|
||||||
if (!expect_contains(recallResp, "\"confidence\":0.98", "recall_context returns payload")) return 1;
|
if (!expect_contains(recallResp, "\"confidence\":0.98", "recall_context returns payload")) return 1;
|
||||||
|
|
||||||
|
const std::string recallMinReq = R"({"namespace":"tests"})";
|
||||||
|
std::string recallMinResp = server.dispatch("kom.memory.v1.recall_context", recallMinReq);
|
||||||
|
if (!expect_contains(recallMinResp, "\"items\"", "recall_context minimum parameters items")) return 1;
|
||||||
|
if (!expect_contains(recallMinResp, "\"id\"", "recall_context minimum parameters id")) return 1;
|
||||||
|
|
||||||
|
const std::string embedReq = R"({"namespace":"tests","texts":["hello world", "goodbye world"]})";
|
||||||
|
std::string embedResp = server.dispatch("kom.memory.v1.embed_text", embedReq);
|
||||||
|
if (!expect_contains(embedResp, "\"model\"", "embed_text model key")) return 1;
|
||||||
|
if (!expect_contains(embedResp, "\"vectors\"", "embed_text vectors key")) return 1;
|
||||||
|
if (!expect_contains(embedResp, "[[", "embed_text vectors content")) return 1;
|
||||||
|
|
||||||
std::string firstId;
|
std::string firstId;
|
||||||
const std::string idsAnchor = "\"ids\":[\"";
|
const std::string idsAnchor = "\"ids\":[\"";
|
||||||
auto idsPos = upsertResp.find(idsAnchor);
|
auto idsPos = upsertResp.find(idsAnchor);
|
||||||
|
|
@ -63,6 +74,10 @@ const std::string idsAnchor = "\"ids\":[\"";
|
||||||
std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq);
|
std::string vectorResp = server.dispatch("kom.memory.v1.search_memory", vectorReq);
|
||||||
if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1;
|
if (!expect_contains(vectorResp, "\"id\":\""+firstId+"\"", "vector search returns stored id")) return 1;
|
||||||
|
|
||||||
|
const std::string warmCacheReq = R"({"namespace":"tests"})";
|
||||||
|
std::string warmCacheResp = server.dispatch("kom.memory.v1.warm_cache", warmCacheReq);
|
||||||
|
if (!expect_contains(warmCacheResp, "\"queued\"", "warm_cache queued key")) return 1;
|
||||||
|
|
||||||
const std::string snapshotResp = server.dispatch("kom.meta.v1.project_snapshot", "{}");
|
const std::string snapshotResp = server.dispatch("kom.meta.v1.project_snapshot", "{}");
|
||||||
if (!expect_contains(snapshotResp, "\"sections\"", "snapshot sections key")) return 1;
|
if (!expect_contains(snapshotResp, "\"sections\"", "snapshot sections key")) return 1;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue