notex-companion 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/dist/cli.js +199 -21
- package/dist/client.js +3 -1
- package/dist/index.js +6 -2
- package/dist/mcpTools.d.ts +6 -8
- package/dist/notexClient.d.ts +43 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A local retrieval companion for [Notex](https://github.com/bhirmbani/notex). It reads a checkout's
|
|
4
4
|
`graphify-out/graph.json` and serves deterministic search/query/path/node lookups over it — to the
|
|
5
|
-
Notex browser page over loopback HTTP, and
|
|
6
|
-
lands) to an MCP host over stdio.
|
|
5
|
+
Notex browser page over loopback HTTP, and to an MCP host over stdio.
|
|
7
6
|
|
|
8
7
|
## What it reads
|
|
9
8
|
|
|
@@ -42,10 +41,15 @@ of starting a broken server.
|
|
|
42
41
|
|
|
43
42
|
```bash
|
|
44
43
|
notex-companion [serve] [options] # start the loopback HTTP server — the default command
|
|
45
|
-
notex-companion mcp # start the stdio MCP server
|
|
46
|
-
#
|
|
44
|
+
notex-companion mcp # start the stdio MCP server — graph_status, graph_search,
|
|
45
|
+
# graph_query, graph_path, graph_node, plus notex_list_questions,
|
|
46
|
+
# notex_get_question, notex_get_answer, notex_save_answer
|
|
47
|
+
# (require .notex/notex.json — see docs/specs/notex-mcp-server.md)
|
|
47
48
|
```
|
|
48
49
|
|
|
50
|
+
Setup, how it picks which checkout to serve, and a manual verification walkthrough:
|
|
51
|
+
[`docs/testing/mcp-server-setup.md`](https://github.com/bhirmbani/notex/blob/main/docs/testing/mcp-server-setup.md).
|
|
52
|
+
|
|
49
53
|
### `serve` options
|
|
50
54
|
|
|
51
55
|
| Flag | Default | Meaning |
|
|
@@ -63,7 +67,7 @@ browser's `localStorage`, keyed by Repository, and it is never sent to the Notex
|
|
|
63
67
|
|
|
64
68
|
## `apiVersion`
|
|
65
69
|
|
|
66
|
-
`ping` and every op response report `apiVersion` (currently `0.1
|
|
70
|
+
`ping` and every op response report `apiVersion` (currently `0.2.1`). Compatibility rule — see
|
|
67
71
|
[`companion-api.md` §1.1](https://github.com/bhirmbani/notex/blob/main/docs/specs/companion-api.md#11-apiversion-compatibility-rule-decided-by-tbr-66)
|
|
68
72
|
for the full rationale:
|
|
69
73
|
|
package/dist/cli.js
CHANGED
|
@@ -6580,7 +6580,9 @@ var ERROR_CODES = {
|
|
|
6580
6580
|
notFound: "not_found",
|
|
6581
6581
|
graphUnreadable: "graph_unreadable",
|
|
6582
6582
|
invalidRequest: "invalid_request",
|
|
6583
|
-
graphLoading: "graph_loading"
|
|
6583
|
+
graphLoading: "graph_loading",
|
|
6584
|
+
forbidden: "forbidden",
|
|
6585
|
+
notexApiError: "notex_api_error"
|
|
6584
6586
|
};
|
|
6585
6587
|
|
|
6586
6588
|
class OpError extends Error {
|
|
@@ -7012,7 +7014,9 @@ var ERROR_STATUS = {
|
|
|
7012
7014
|
[ERROR_CODES.notFound]: 404,
|
|
7013
7015
|
[ERROR_CODES.graphUnreadable]: 409,
|
|
7014
7016
|
[ERROR_CODES.invalidRequest]: 422,
|
|
7015
|
-
[ERROR_CODES.graphLoading]: 503
|
|
7017
|
+
[ERROR_CODES.graphLoading]: 503,
|
|
7018
|
+
[ERROR_CODES.forbidden]: 403,
|
|
7019
|
+
[ERROR_CODES.notexApiError]: 502
|
|
7016
7020
|
};
|
|
7017
7021
|
function createHandler(opts) {
|
|
7018
7022
|
return async (req) => {
|
|
@@ -7188,10 +7192,10 @@ function startNodeServer(opts) {
|
|
|
7188
7192
|
}
|
|
7189
7193
|
};
|
|
7190
7194
|
}
|
|
7191
|
-
async function handleRequest(req, res,
|
|
7195
|
+
async function handleRequest(req, res, fetch2) {
|
|
7192
7196
|
try {
|
|
7193
7197
|
const request = await toWebRequest(req);
|
|
7194
|
-
const response = await
|
|
7198
|
+
const response = await fetch2(request);
|
|
7195
7199
|
res.statusCode = response.status;
|
|
7196
7200
|
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
7197
7201
|
res.end(Buffer.from(await response.arrayBuffer()));
|
|
@@ -33794,34 +33798,107 @@ function createGraphTools(ctx) {
|
|
|
33794
33798
|
};
|
|
33795
33799
|
}
|
|
33796
33800
|
var LINK_MESSAGE = "Not linked to a Notex Repository — run `npx notex-companion link`";
|
|
33797
|
-
|
|
33798
|
-
|
|
33799
|
-
|
|
33800
|
-
|
|
33801
|
-
|
|
33802
|
-
|
|
33803
|
-
|
|
33804
|
-
|
|
33805
|
-
|
|
33806
|
-
|
|
33801
|
+
var MAX_INLINE_ANSWER_BYTES = 4096;
|
|
33802
|
+
function requireLinked(ctx) {
|
|
33803
|
+
const state = ctx.getConfigState();
|
|
33804
|
+
if (state.kind === "unlinked")
|
|
33805
|
+
return { ok: false, error: { isError: true, content: [{ type: "text", text: LINK_MESSAGE }] } };
|
|
33806
|
+
return { ok: true, config: state.config };
|
|
33807
|
+
}
|
|
33808
|
+
function fromNotexError(err) {
|
|
33809
|
+
if (err instanceof OpError)
|
|
33810
|
+
return errorResult(err);
|
|
33811
|
+
throw err;
|
|
33812
|
+
}
|
|
33813
|
+
function truncateUtf8(text, maxBytes) {
|
|
33814
|
+
const buf = Buffer.from(text, "utf8");
|
|
33815
|
+
if (buf.length <= maxBytes)
|
|
33816
|
+
return text;
|
|
33817
|
+
let end = maxBytes;
|
|
33818
|
+
while (end > 0 && (buf[end - 1] & 192) === 128)
|
|
33819
|
+
end--;
|
|
33820
|
+
if (end > 0) {
|
|
33821
|
+
const lead = buf[end - 1];
|
|
33822
|
+
const seqLen = lead >= 240 ? 4 : lead >= 224 ? 3 : lead >= 192 ? 2 : 1;
|
|
33823
|
+
if (end - 1 + seqLen > maxBytes)
|
|
33824
|
+
end--;
|
|
33825
|
+
}
|
|
33826
|
+
return buf.subarray(0, end).toString("utf8");
|
|
33827
|
+
}
|
|
33828
|
+
function summarizeAnswer(file2) {
|
|
33829
|
+
if (file2.contentType === "upload")
|
|
33830
|
+
return { id: file2.id, name: file2.name, contentType: "upload", createdAt: file2.createdAt };
|
|
33831
|
+
const bytes = Buffer.byteLength(file2.content, "utf8");
|
|
33832
|
+
if (bytes <= MAX_INLINE_ANSWER_BYTES) {
|
|
33833
|
+
return { id: file2.id, name: file2.name, contentType: "text", content: file2.content, createdAt: file2.createdAt };
|
|
33834
|
+
}
|
|
33835
|
+
return { id: file2.id, name: file2.name, contentType: "text", content: truncateUtf8(file2.content, MAX_INLINE_ANSWER_BYTES), truncated: true, createdAt: file2.createdAt };
|
|
33836
|
+
}
|
|
33837
|
+
function createNotexTools(ctx) {
|
|
33807
33838
|
return {
|
|
33808
33839
|
notex_list_questions: {
|
|
33809
33840
|
description: "List the bound Repository's Questions.",
|
|
33810
33841
|
inputSchema: {},
|
|
33811
|
-
handler:
|
|
33842
|
+
handler: async () => {
|
|
33843
|
+
const linked = requireLinked(ctx);
|
|
33844
|
+
if (!linked.ok)
|
|
33845
|
+
return linked.error;
|
|
33846
|
+
try {
|
|
33847
|
+
const client = ctx.getNotexClient(linked.config);
|
|
33848
|
+
const questions = await client.listContexts(linked.config.organizationId, linked.config.repositoryId);
|
|
33849
|
+
const text = questions.length === 0 ? "No Questions yet." : questions.map((q) => `${q.id} — ${q.question}`).join(`
|
|
33850
|
+
`);
|
|
33851
|
+
return { content: [{ type: "text", text }], structuredContent: { questions } };
|
|
33852
|
+
} catch (err) {
|
|
33853
|
+
return fromNotexError(err);
|
|
33854
|
+
}
|
|
33855
|
+
}
|
|
33812
33856
|
},
|
|
33813
33857
|
notex_get_question: {
|
|
33814
33858
|
description: "A Question plus its Answers.",
|
|
33815
33859
|
inputSchema: { questionId: exports_external.string().min(1) },
|
|
33816
|
-
handler:
|
|
33860
|
+
handler: async (args) => {
|
|
33861
|
+
const linked = requireLinked(ctx);
|
|
33862
|
+
if (!linked.ok)
|
|
33863
|
+
return linked.error;
|
|
33864
|
+
const { questionId } = args;
|
|
33865
|
+
try {
|
|
33866
|
+
const client = ctx.getNotexClient(linked.config);
|
|
33867
|
+
const question = await client.getContext(linked.config.organizationId, questionId);
|
|
33868
|
+
if (question.repositoryId !== linked.config.repositoryId) {
|
|
33869
|
+
return errorResult(new OpError("not_found", `Unknown Question id: ${questionId}`));
|
|
33870
|
+
}
|
|
33871
|
+
const files = await client.listFiles(linked.config.organizationId, questionId);
|
|
33872
|
+
const answers = files.map(summarizeAnswer);
|
|
33873
|
+
const text = `${question.question}
|
|
33874
|
+
|
|
33875
|
+
${answers.length} answer(s)`;
|
|
33876
|
+
return { content: [{ type: "text", text }], structuredContent: { question, answers } };
|
|
33877
|
+
} catch (err) {
|
|
33878
|
+
return fromNotexError(err);
|
|
33879
|
+
}
|
|
33880
|
+
}
|
|
33817
33881
|
},
|
|
33818
33882
|
notex_get_answer: {
|
|
33819
33883
|
description: "Full Answer content.",
|
|
33820
33884
|
inputSchema: { answerId: exports_external.string().min(1) },
|
|
33821
|
-
handler:
|
|
33885
|
+
handler: async (args) => {
|
|
33886
|
+
const linked = requireLinked(ctx);
|
|
33887
|
+
if (!linked.ok)
|
|
33888
|
+
return linked.error;
|
|
33889
|
+
const { answerId } = args;
|
|
33890
|
+
try {
|
|
33891
|
+
const client = ctx.getNotexClient(linked.config);
|
|
33892
|
+
const answer = await client.getFile(linked.config.organizationId, answerId);
|
|
33893
|
+
const text = answer.contentType === "upload" ? `${answer.name} (upload, no readable content)` : answer.content;
|
|
33894
|
+
return { content: [{ type: "text", text }], structuredContent: { answer } };
|
|
33895
|
+
} catch (err) {
|
|
33896
|
+
return fromNotexError(err);
|
|
33897
|
+
}
|
|
33898
|
+
}
|
|
33822
33899
|
},
|
|
33823
33900
|
notex_save_answer: {
|
|
33824
|
-
description:
|
|
33901
|
+
description: 'Save a graph-drafted Answer. Strictly additive — always creates a new Answer, never updates or deletes. Exactly one of question/questionId. `name` is the Answer\'s title, shown alongside the question in the Notex UI — write a short distinct label (e.g. "Auth flow overview"), not a restatement of the question text. `content` is the drafted prose itself, without the footer.',
|
|
33825
33902
|
inputSchema: {
|
|
33826
33903
|
question: exports_external.string().min(1).optional(),
|
|
33827
33904
|
questionId: exports_external.string().min(1).optional(),
|
|
@@ -33829,11 +33906,111 @@ function createNotexToolStubs(ctx) {
|
|
|
33829
33906
|
content: exports_external.string().min(1),
|
|
33830
33907
|
sourceNodeIds: exports_external.array(exports_external.string()).min(1)
|
|
33831
33908
|
},
|
|
33832
|
-
handler:
|
|
33909
|
+
handler: async (args) => {
|
|
33910
|
+
const linked = requireLinked(ctx);
|
|
33911
|
+
if (!linked.ok)
|
|
33912
|
+
return linked.error;
|
|
33913
|
+
const { config: config3 } = linked;
|
|
33914
|
+
const a = args;
|
|
33915
|
+
if (a.question === undefined === (a.questionId === undefined)) {
|
|
33916
|
+
return errorResult(new OpError("invalid_request", "Exactly one of question or questionId is required"));
|
|
33917
|
+
}
|
|
33918
|
+
const graphState = ctx.getGraphState();
|
|
33919
|
+
if (graphState.kind === "error")
|
|
33920
|
+
return errorResult(graphState.error);
|
|
33921
|
+
const { index } = graphState;
|
|
33922
|
+
const uniqueIds = [...new Set(a.sourceNodeIds)];
|
|
33923
|
+
const unresolved = uniqueIds.filter((id) => !ctx.retrievalLog.has(id));
|
|
33924
|
+
if (unresolved.length > 0) {
|
|
33925
|
+
return errorResult(new OpError("invalid_request", `sourceNodeIds cites id(s) not returned by this session's graph_query/graph_node/graph_path: ${unresolved.join(", ")}`));
|
|
33926
|
+
}
|
|
33927
|
+
const sources = uniqueIds.map((id) => {
|
|
33928
|
+
const projected = index.project(index.nodesById.get(id));
|
|
33929
|
+
return { file: projected.sourceFile, location: projected.sourceLocation };
|
|
33930
|
+
});
|
|
33931
|
+
const footer = buildFooter(index.stamp, sources);
|
|
33932
|
+
const fullContent = a.content + footer;
|
|
33933
|
+
try {
|
|
33934
|
+
const client = ctx.getNotexClient(config3);
|
|
33935
|
+
let targetQuestionId;
|
|
33936
|
+
let createdNewQuestion = false;
|
|
33937
|
+
if (a.question !== undefined) {
|
|
33938
|
+
const created = await client.createContext(config3.organizationId, config3.repositoryId, a.question);
|
|
33939
|
+
targetQuestionId = created.id;
|
|
33940
|
+
createdNewQuestion = true;
|
|
33941
|
+
} else {
|
|
33942
|
+
const existing = await client.getContext(config3.organizationId, a.questionId);
|
|
33943
|
+
if (existing.repositoryId !== config3.repositoryId) {
|
|
33944
|
+
return errorResult(new OpError("not_found", `Unknown Question id: ${a.questionId}`));
|
|
33945
|
+
}
|
|
33946
|
+
targetQuestionId = existing.id;
|
|
33947
|
+
}
|
|
33948
|
+
let file2;
|
|
33949
|
+
try {
|
|
33950
|
+
file2 = await client.createFile(config3.organizationId, targetQuestionId, {
|
|
33951
|
+
name: a.name,
|
|
33952
|
+
contentType: "text",
|
|
33953
|
+
content: fullContent
|
|
33954
|
+
});
|
|
33955
|
+
} catch (fileErr) {
|
|
33956
|
+
if (createdNewQuestion)
|
|
33957
|
+
await client.deleteContext(config3.organizationId, targetQuestionId).catch(() => {});
|
|
33958
|
+
throw fileErr;
|
|
33959
|
+
}
|
|
33960
|
+
const text = `Saved Answer "${file2.name}" (${file2.id}) to Question ${targetQuestionId}.`;
|
|
33961
|
+
return { content: [{ type: "text", text }], structuredContent: { answerId: file2.id, questionId: targetQuestionId, footer } };
|
|
33962
|
+
} catch (err) {
|
|
33963
|
+
return fromNotexError(err);
|
|
33964
|
+
}
|
|
33965
|
+
}
|
|
33833
33966
|
}
|
|
33834
33967
|
};
|
|
33835
33968
|
}
|
|
33836
33969
|
|
|
33970
|
+
// src/notexClient.ts
|
|
33971
|
+
var DEFAULT_API_URL = "http://localhost:3000";
|
|
33972
|
+
function resolveApiUrl(env = process.env) {
|
|
33973
|
+
return env.NOTEX_API_URL?.trim() || DEFAULT_API_URL;
|
|
33974
|
+
}
|
|
33975
|
+
async function request(config3, fetchImpl, method, path2, body) {
|
|
33976
|
+
let res;
|
|
33977
|
+
try {
|
|
33978
|
+
res = await fetchImpl(`${config3.baseUrl}${path2}`, {
|
|
33979
|
+
method,
|
|
33980
|
+
headers: {
|
|
33981
|
+
"x-api-key": config3.apiKey,
|
|
33982
|
+
...body !== undefined ? { "content-type": "application/json" } : {}
|
|
33983
|
+
},
|
|
33984
|
+
body: body !== undefined ? JSON.stringify(body) : undefined
|
|
33985
|
+
});
|
|
33986
|
+
} catch (err) {
|
|
33987
|
+
throw new OpError(ERROR_CODES.notexApiError, `Could not reach the Notex API at ${config3.baseUrl}`, err);
|
|
33988
|
+
}
|
|
33989
|
+
if (res.status === 401)
|
|
33990
|
+
throw new OpError(ERROR_CODES.unauthorized, "Notex rejected the API key");
|
|
33991
|
+
if (res.status === 403)
|
|
33992
|
+
throw new OpError(ERROR_CODES.forbidden, "Not authorized for this Project");
|
|
33993
|
+
if (res.status === 404)
|
|
33994
|
+
throw new OpError(ERROR_CODES.notFound, "Not found in Notex");
|
|
33995
|
+
if (!res.ok) {
|
|
33996
|
+
const body2 = await res.json().catch(() => ({}));
|
|
33997
|
+
throw new OpError(ERROR_CODES.notexApiError, body2.error?.message ?? `Notex API error (HTTP ${res.status})`);
|
|
33998
|
+
}
|
|
33999
|
+
return await res.json();
|
|
34000
|
+
}
|
|
34001
|
+
var enc = (id) => encodeURIComponent(id);
|
|
34002
|
+
function createNotexClient(config3, fetchImpl = fetch) {
|
|
34003
|
+
return {
|
|
34004
|
+
listContexts: (organizationId, repositoryId) => request(config3, fetchImpl, "GET", `/api/v1/organizations/${enc(organizationId)}/repositories/${enc(repositoryId)}/contexts`),
|
|
34005
|
+
getContext: (organizationId, id) => request(config3, fetchImpl, "GET", `/api/v1/organizations/${enc(organizationId)}/contexts/${enc(id)}`),
|
|
34006
|
+
createContext: (organizationId, repositoryId, question) => request(config3, fetchImpl, "POST", `/api/v1/organizations/${enc(organizationId)}/repositories/${enc(repositoryId)}/contexts`, { question }),
|
|
34007
|
+
listFiles: (organizationId, contextId) => request(config3, fetchImpl, "GET", `/api/v1/organizations/${enc(organizationId)}/contexts/${enc(contextId)}/files`),
|
|
34008
|
+
getFile: (organizationId, id) => request(config3, fetchImpl, "GET", `/api/v1/organizations/${enc(organizationId)}/files/${enc(id)}`),
|
|
34009
|
+
createFile: (organizationId, contextId, file2) => request(config3, fetchImpl, "POST", `/api/v1/organizations/${enc(organizationId)}/contexts/${enc(contextId)}/files`, file2),
|
|
34010
|
+
deleteContext: (organizationId, id) => request(config3, fetchImpl, "DELETE", `/api/v1/organizations/${enc(organizationId)}/contexts/${enc(id)}`)
|
|
34011
|
+
};
|
|
34012
|
+
}
|
|
34013
|
+
|
|
33837
34014
|
// src/notexConfig.ts
|
|
33838
34015
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
33839
34016
|
import { join as join2 } from "node:path";
|
|
@@ -33899,10 +34076,11 @@ function buildMcpServer(checkoutPath) {
|
|
|
33899
34076
|
getCurrentHeadSha: createHeadShaCache(checkoutPath),
|
|
33900
34077
|
getGraphState: () => graphState,
|
|
33901
34078
|
getConfigState: () => loadNotexConfig(checkoutPath),
|
|
33902
|
-
retrievalLog
|
|
34079
|
+
retrievalLog,
|
|
34080
|
+
getNotexClient: (config3) => createNotexClient({ baseUrl: resolveApiUrl(), apiKey: config3.apiKey })
|
|
33903
34081
|
};
|
|
33904
34082
|
const server = new McpServer({ name: "notex-companion", version: API_VERSION });
|
|
33905
|
-
registerMcpTools(server, { ...createGraphTools(ctx), ...
|
|
34083
|
+
registerMcpTools(server, { ...createGraphTools(ctx), ...createNotexTools(ctx) });
|
|
33906
34084
|
return server;
|
|
33907
34085
|
}
|
|
33908
34086
|
async function startMcpServer(checkoutPath) {
|
package/dist/client.js
CHANGED
|
@@ -4,7 +4,9 @@ var ERROR_CODES = {
|
|
|
4
4
|
notFound: "not_found",
|
|
5
5
|
graphUnreadable: "graph_unreadable",
|
|
6
6
|
invalidRequest: "invalid_request",
|
|
7
|
-
graphLoading: "graph_loading"
|
|
7
|
+
graphLoading: "graph_loading",
|
|
8
|
+
forbidden: "forbidden",
|
|
9
|
+
notexApiError: "notex_api_error"
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
class OpError extends Error {
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,9 @@ var ERROR_CODES = {
|
|
|
4
4
|
notFound: "not_found",
|
|
5
5
|
graphUnreadable: "graph_unreadable",
|
|
6
6
|
invalidRequest: "invalid_request",
|
|
7
|
-
graphLoading: "graph_loading"
|
|
7
|
+
graphLoading: "graph_loading",
|
|
8
|
+
forbidden: "forbidden",
|
|
9
|
+
notexApiError: "notex_api_error"
|
|
8
10
|
};
|
|
9
11
|
|
|
10
12
|
class OpError extends Error {
|
|
@@ -485,7 +487,9 @@ var ERROR_STATUS = {
|
|
|
485
487
|
[ERROR_CODES.notFound]: 404,
|
|
486
488
|
[ERROR_CODES.graphUnreadable]: 409,
|
|
487
489
|
[ERROR_CODES.invalidRequest]: 422,
|
|
488
|
-
[ERROR_CODES.graphLoading]: 503
|
|
490
|
+
[ERROR_CODES.graphLoading]: 503,
|
|
491
|
+
[ERROR_CODES.forbidden]: 403,
|
|
492
|
+
[ERROR_CODES.notexApiError]: 502
|
|
489
493
|
};
|
|
490
494
|
function createHandler(opts) {
|
|
491
495
|
return async (req) => {
|
package/dist/mcpTools.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { OpError } from "./types.js";
|
|
3
3
|
import type { GraphIndex } from "./graph.js";
|
|
4
|
-
import type { NotexConfigState } from "./notexConfig.js";
|
|
4
|
+
import type { NotexConfig, NotexConfigState } from "./notexConfig.js";
|
|
5
|
+
import type { NotexClient } from "./notexClient.js";
|
|
5
6
|
import type { RetrievalLog } from "./retrievalLog.js";
|
|
6
7
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
7
8
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -21,6 +22,9 @@ export type McpToolContext = {
|
|
|
21
22
|
* run mid-session without a restart. */
|
|
22
23
|
getConfigState: () => NotexConfigState;
|
|
23
24
|
retrievalLog: RetrievalLog;
|
|
25
|
+
/** Built fresh from the linked config on every notex_* call — a seam mcpTools.test.ts uses to
|
|
26
|
+
* inject a fake client, and the same "re-verify, don't cache" posture as getConfigState. */
|
|
27
|
+
getNotexClient: (config: NotexConfig) => NotexClient;
|
|
24
28
|
};
|
|
25
29
|
export type ToolDef = {
|
|
26
30
|
description: string;
|
|
@@ -29,10 +33,4 @@ export type ToolDef = {
|
|
|
29
33
|
};
|
|
30
34
|
export declare function registerMcpTools(server: McpServer, tools: Record<string, ToolDef>): void;
|
|
31
35
|
export declare function createGraphTools(ctx: McpToolContext): Record<string, ToolDef>;
|
|
32
|
-
|
|
33
|
-
* Argument shapes only, per §2.3/§2.4 — every handler errors. The actual Notex Worker binding
|
|
34
|
-
* (auth, requests, the retrieval-log-checked write) is TBR-72's job. What TBR-69 owes here is
|
|
35
|
-
* §4.1's acceptance bar: the tools are listed, never hidden, and a missing/malformed
|
|
36
|
-
* `.notex/notex.json` produces the documented actionable error rather than a stack trace.
|
|
37
|
-
*/
|
|
38
|
-
export declare function createNotexToolStubs(ctx: McpToolContext): Record<string, ToolDef>;
|
|
36
|
+
export declare function createNotexTools(ctx: McpToolContext): Record<string, ToolDef>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type NotexContext = {
|
|
2
|
+
id: string;
|
|
3
|
+
repositoryId: string;
|
|
4
|
+
question: string;
|
|
5
|
+
createdAt: string;
|
|
6
|
+
};
|
|
7
|
+
export type NotexFile = {
|
|
8
|
+
id: string;
|
|
9
|
+
contextId: string;
|
|
10
|
+
name: string;
|
|
11
|
+
contentType: "text" | "upload";
|
|
12
|
+
content: string;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
};
|
|
15
|
+
export type NotexClientConfig = {
|
|
16
|
+
baseUrl: string;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
};
|
|
19
|
+
type FetchImpl = typeof fetch;
|
|
20
|
+
/** `NOTEX_API_URL` overrides the default — there is no production Notex origin baked into this
|
|
21
|
+
* package yet, and `.notex/notex.json` (notex-mcp-server.md §4) deliberately carries no URL. */
|
|
22
|
+
export declare function resolveApiUrl(env?: {
|
|
23
|
+
NOTEX_API_URL?: string;
|
|
24
|
+
}): string;
|
|
25
|
+
export declare function createNotexClient(config: NotexClientConfig, fetchImpl?: FetchImpl): {
|
|
26
|
+
listContexts: (organizationId: string, repositoryId: string) => Promise<NotexContext[]>;
|
|
27
|
+
getContext: (organizationId: string, id: string) => Promise<NotexContext>;
|
|
28
|
+
createContext: (organizationId: string, repositoryId: string, question: string) => Promise<NotexContext>;
|
|
29
|
+
listFiles: (organizationId: string, contextId: string) => Promise<NotexFile[]>;
|
|
30
|
+
getFile: (organizationId: string, id: string) => Promise<NotexFile>;
|
|
31
|
+
createFile: (organizationId: string, contextId: string, file: {
|
|
32
|
+
name: string;
|
|
33
|
+
contentType: "text" | "upload";
|
|
34
|
+
content: string;
|
|
35
|
+
}) => Promise<NotexFile>;
|
|
36
|
+
/** Used only for best-effort rollback of a Question just created by notex_save_answer when
|
|
37
|
+
* its Answer write then fails — never exposed as its own MCP tool (§2.4/§9: no delete tool). */
|
|
38
|
+
deleteContext: (organizationId: string, id: string) => Promise<{
|
|
39
|
+
success: true;
|
|
40
|
+
}>;
|
|
41
|
+
};
|
|
42
|
+
export type NotexClient = ReturnType<typeof createNotexClient>;
|
|
43
|
+
export {};
|
package/dist/types.d.ts
CHANGED
|
@@ -55,6 +55,11 @@ export declare const ERROR_CODES: {
|
|
|
55
55
|
readonly graphUnreadable: "graph_unreadable";
|
|
56
56
|
readonly invalidRequest: "invalid_request";
|
|
57
57
|
readonly graphLoading: "graph_loading";
|
|
58
|
+
/** The Notex API rejected a request as 403 — key holder lacks Grant access to the Project. */
|
|
59
|
+
readonly forbidden: "forbidden";
|
|
60
|
+
/** Any other non-2xx from the Notex API (rate limit, 5xx, ...) — notex-mcp-server.md §4.1
|
|
61
|
+
* reuses this module's error vocabulary rather than inventing a second taxonomy. */
|
|
62
|
+
readonly notexApiError: "notex_api_error";
|
|
58
63
|
};
|
|
59
64
|
export type ErrorCode = (typeof ERROR_CODES)[keyof typeof ERROR_CODES];
|
|
60
65
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "notex-companion",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Local retrieval companion for Notex — reads a checkout's graphify-out/graph.json and serves deterministic search/query/path/node lookups over loopback HTTP and MCP stdio. No LLM, no graph building, no network beyond 127.0.0.1.",
|
|
5
5
|
"keywords": ["notex", "graphify", "mcp", "code-graph"],
|
|
6
6
|
"license": "MIT",
|